Skip to content

Commit 66c2f9b

Browse files
committed
Initial public release
1 parent de80063 commit 66c2f9b

File tree

21 files changed

+1078
-2
lines changed

21 files changed

+1078
-2
lines changed

.github/workflows/ci.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
name: ci
2+
on: push
3+
jobs:
4+
build:
5+
runs-on: ubuntu-latest
6+
steps:
7+
8+
# Checkout the source code
9+
- name: Checkout
10+
uses: actions/checkout@v3
11+
12+
# Install Go 1.20
13+
- name: Setup Go
14+
uses: actions/setup-go@v3
15+
with:
16+
go-version: "1.20"
17+
18+
# Build binaries for both Linux and Windows
19+
- name: Build for all platforms
20+
run: go mod download && go run build.go -release
21+
22+
# If this is a tagged release then upload the release binaries
23+
- name: Upload release binaries
24+
uses: softprops/action-gh-release@v1
25+
if: startsWith(github.ref, 'refs/tags/')
26+
with:
27+
files: ./bin/bootnext-*

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
.build/
2+
bin/

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2023 TensorWorks Pty Ltd
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

README.md

Lines changed: 258 additions & 2 deletions
Large diffs are not rendered by default.

build.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
//go:build never
2+
3+
package main
4+
5+
import (
6+
"flag"
7+
"os"
8+
9+
module "github.com/tensorworks/go-build-helpers/pkg/module"
10+
validation "github.com/tensorworks/go-build-helpers/pkg/validation"
11+
)
12+
13+
// Alias validation.ExitIfError() as check()
14+
var check = validation.ExitIfError
15+
16+
func main() {
17+
18+
// Parse our command-line flags
19+
doClean := flag.Bool("clean", false, "cleans build outputs")
20+
doRelease := flag.Bool("release", false, "builds executables for all target platforms")
21+
flag.Parse()
22+
23+
// Disable CGO
24+
os.Setenv("CGO_ENABLED", "0")
25+
26+
// Create a build helper for the Go module in the current working directory
27+
mod, err := module.ModuleInCwd()
28+
check(err)
29+
30+
// Determine if we're cleaning the build outputs
31+
if *doClean == true {
32+
check(mod.CleanAll())
33+
os.Exit(0)
34+
}
35+
36+
// Install the `go-winres` tool that we use for embedding manifest data in Windows builds
37+
check(mod.InstallGoTools([]string{
38+
"github.com/tc-hib/go-winres@v0.3.1",
39+
}))
40+
41+
// Run `go generate` to invoke `go-winres`
42+
check(mod.Generate())
43+
44+
// Determine if we're building our executables for just the host platform or for the full matrix of release platforms
45+
if *doRelease == false {
46+
check(mod.BuildBinariesForHost(module.DefaultBinDir, module.BuildOptions{Scheme: module.Undecorated}))
47+
} else {
48+
check(mod.BuildBinariesForMatrix(
49+
module.DefaultBinDir,
50+
51+
module.BuildOptions{
52+
AdditionalFlags: []string{"-ldflags", "-s -w"},
53+
Scheme: module.SuffixedFilenames,
54+
},
55+
56+
module.BuildMatrix{
57+
Platforms: []string{"linux", "windows"},
58+
Architectures: []string{"386", "amd64", "arm64"},
59+
Ignore: []string{},
60+
},
61+
))
62+
}
63+
}

cmd/bootnext/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
*.syso

cmd/bootnext/generate.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
//go:generate go-winres simply --arch 386,amd64,arm64 --product-name "BootNext" --file-description "BootNext"
2+
3+
package main

cmd/bootnext/main.go

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"regexp"
8+
"runtime"
9+
"strings"
10+
11+
"github.com/spf13/cobra"
12+
"github.com/tensorworks/bootnext/internal/constants"
13+
"github.com/tensorworks/bootnext/internal/elevate"
14+
"github.com/tensorworks/bootnext/internal/process"
15+
"github.com/tensorworks/bootnext/internal/reboot"
16+
"github.com/tensorworks/bootnext/internal/uefi"
17+
)
18+
19+
func run(pattern string, dryRun bool, listOnly bool, noElevate bool, noReboot bool) error {
20+
21+
// Verify that the operating system has been booted in UEFI mode
22+
enabled, err := uefi.IsUEFIEnabled()
23+
if err != nil {
24+
return fmt.Errorf("failed to query system UEFI status: %v", err)
25+
} else if !enabled {
26+
return fmt.Errorf("unsupported system configuration: the operating system has not been booted in UEFI mode")
27+
}
28+
29+
// Verify that all of the system tools we require for interacting with UEFI NVRAM variables are available
30+
requiredTools := uefi.RequiredTools()
31+
for _, tool := range requiredTools {
32+
if _, err := exec.LookPath(tool); err != nil {
33+
return fmt.Errorf("a required application was not found in the system PATH: %v", tool)
34+
}
35+
}
36+
37+
// Determine whether we require elevated privileges
38+
// (We need them for writing to NVRAM variables under Linux, and for both reading and writing under Windows)
39+
requireElevation := (!dryRun && !listOnly) || runtime.GOOS == "windows"
40+
41+
// Determine whether the process is running with insufficient privileges
42+
if requireElevation && !elevate.IsElevated() {
43+
44+
// Determine whether we should automatically request elevated privileges
45+
if !noElevate {
46+
47+
// Re-run the process with elevated privileges and propagate the exit code
48+
exitCode, err := elevate.RunElevated()
49+
if err != nil {
50+
return fmt.Errorf("failed to re-launch the process with elevated privileges: %v", err)
51+
} else {
52+
os.Exit(exitCode)
53+
}
54+
55+
} else {
56+
fmt.Print("Warning: running without elevated privileges, access to UEFI NVRAM variables may be denied.\n\n")
57+
}
58+
}
59+
60+
// Retrieve the list of UEFI boot entries
61+
entries, err := uefi.ListBootEntries()
62+
if err != nil {
63+
return fmt.Errorf("failed to list UEFI boot entries: %v", err)
64+
}
65+
66+
// Print the list of boot entries
67+
fmt.Println("Detected the following UEFI boot entries:")
68+
for _, entry := range entries {
69+
fmt.Print("- ID: \"", entry.ID, "\", Description: \"", entry.Description, "\"\n")
70+
}
71+
72+
// If we are just listing the boot entries then stop here
73+
if listOnly {
74+
return nil
75+
}
76+
77+
// Compile the regular expression pattern supplied by the user, enabling case-insensitive matching
78+
regex, err := regexp.Compile(fmt.Sprintf("(?i)%s", pattern))
79+
if err != nil {
80+
return fmt.Errorf("failed to compile regular expression \"%s\": %v", pattern, err)
81+
}
82+
83+
// Identify the first boot entry that matches the pattern
84+
fmt.Printf("\nMatching boot entries against regular expression \"%s\"\n", pattern)
85+
for _, entry := range entries {
86+
if regex.MatchString(entry.Description) {
87+
88+
// Print the matching boot entry
89+
fmt.Printf("Found matching boot entry: \"%s\"\n", entry.Description)
90+
91+
// Don't modify the BootNext variable or reboot if we are performing a dry run
92+
if !dryRun {
93+
94+
// Set the value of the BootNext variable to the entry's identifier
95+
fmt.Println("Setting the BootNext variable...")
96+
if err := uefi.SetBootNext(entry); err != nil {
97+
return fmt.Errorf("failed to set BootNext variable value: %v", err)
98+
}
99+
100+
// Determine whether we are triggering a reboot
101+
if !noReboot {
102+
fmt.Println("Rebooting now...")
103+
if err := reboot.Reboot(); err != nil {
104+
return fmt.Errorf("failed to reboot: %v", err)
105+
}
106+
}
107+
}
108+
109+
return nil
110+
}
111+
}
112+
113+
// If we reach this point then none of the boot entries matched the pattern
114+
return fmt.Errorf("could not find any UEFI boot entries matching the pattern \"%s\"", pattern)
115+
}
116+
117+
func main() {
118+
119+
// Define our Cobra command
120+
command := &cobra.Command{
121+
122+
Long: strings.Join([]string{
123+
fmt.Sprintf("bootnext v%s", constants.VERSION),
124+
"Copyright (c) 2023, TensorWorks Pty Ltd",
125+
"",
126+
"Sets the UEFI \"BootNext\" variable and triggers a reboot into the target operating system.",
127+
"This facilitates quickly switching to another OS without modifying the default boot order.",
128+
}, "\n"),
129+
130+
Use: "bootnext pattern",
131+
132+
SilenceUsage: true,
133+
134+
Example: strings.Join([]string{
135+
" bootnext windows Selects the Windows Boot Manager and boots into it",
136+
" bootnext ubuntu Selects the GRUB bootloader installed by Ubuntu Linux and boots into it",
137+
" bootnext USB Selects the first available bootable USB device and boots into it",
138+
}, "\n"),
139+
}
140+
141+
// Inject the usage information for our command's positional arguments
142+
patternUsage := strings.Join([]string{
143+
" pattern A regular expression that will be used to select the target boot entry",
144+
" (case insensitive)",
145+
}, "\n")
146+
template := command.UsageTemplate()
147+
template = strings.Replace(template, "\nFlags:\n", fmt.Sprintf("\nPositional Arguments:\n%s\n\nFlags:\n", patternUsage), 1)
148+
command.SetUsageTemplate(template)
149+
150+
// Define the command-line flags for our command
151+
dryRun := command.Flags().Bool("dry-run", false, "Describe the actions that would be performed but do not make any changes to the system")
152+
listOnly := command.Flags().Bool("list", false, "Print the list of UEFI boot entries but do not set the BootNext variable")
153+
noElevate := command.Flags().Bool("no-elevate", false, "Do not automatically prompt for elevated privileges when required")
154+
noReboot := command.Flags().Bool("no-reboot", false, "Do not automatically reboot after setting the BootNext variable")
155+
pause := command.Flags().Bool("pause", false, "Pause for input when the application is finished running")
156+
157+
// Wire up the validation logic for our command-line flags and positional arguments
158+
command.RunE = func(cmd *cobra.Command, args []string) error {
159+
160+
// If no flags or arguments were specified then print the usage message
161+
if len(os.Args) < 2 {
162+
cmd.Help()
163+
return nil
164+
}
165+
166+
// Verify that a pattern was provided if `--list` was not specified
167+
pattern := ""
168+
if len(args) > 0 {
169+
pattern = args[0]
170+
} else if !*listOnly {
171+
return fmt.Errorf("a pattern must be specified for selecting the target UEFI boot entry")
172+
}
173+
174+
// Process the provided input values and propagate any errors
175+
return run(pattern, *dryRun, *listOnly, *noElevate, *noReboot)
176+
}
177+
178+
// Execute the command
179+
err := command.Execute()
180+
if err != nil {
181+
process.ExitWithPause(1, *pause)
182+
}
183+
184+
process.ExitWithPause(0, *pause)
185+
}

go.mod

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module github.com/tensorworks/bootnext
2+
3+
go 1.18
4+
5+
require (
6+
github.com/spf13/cobra v1.7.0
7+
github.com/tensorworks/go-build-helpers v0.0.5
8+
golang.org/x/sys v0.8.0
9+
)
10+
11+
require (
12+
github.com/inconshreveable/mousetrap v1.1.0 // indirect
13+
github.com/spf13/pflag v1.0.5 // indirect
14+
)

go.sum

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
2+
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
3+
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
4+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
5+
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
6+
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
7+
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
8+
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
9+
github.com/tensorworks/go-build-helpers v0.0.5 h1:6XcKJ0fJ+x6c+cnBOcJpZhJtNoaR+oEZ2gJLjB9Jrng=
10+
github.com/tensorworks/go-build-helpers v0.0.5/go.mod h1:t7C4BkFt5RsSAPOII2D0SgahcdoizZvhejrAjd/Biyc=
11+
golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU=
12+
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
13+
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
14+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

0 commit comments

Comments
 (0)