github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/u-root.go (about) 1 // Copyright 2015-2018 the u-root Authors. All rights reserved 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package main 6 7 import ( 8 "encoding/json" 9 "flag" 10 "fmt" 11 "log" 12 "os" 13 "path" 14 "path/filepath" 15 "runtime" 16 "sort" 17 "strings" 18 "time" 19 20 gbbgolang "github.com/u-root/gobusybox/src/pkg/golang" 21 "github.com/mvdan/u-root-coreutils/pkg/shlex" 22 "github.com/mvdan/u-root-coreutils/pkg/ulog" 23 "github.com/mvdan/u-root-coreutils/pkg/uroot" 24 "github.com/mvdan/u-root-coreutils/pkg/uroot/builder" 25 "github.com/mvdan/u-root-coreutils/pkg/uroot/initramfs" 26 ) 27 28 // multiFlag is used for flags that support multiple invocations, e.g. -files 29 type multiFlag []string 30 31 func (m *multiFlag) String() string { 32 return fmt.Sprint(*m) 33 } 34 35 func (m *multiFlag) Set(value string) error { 36 *m = append(*m, value) 37 return nil 38 } 39 40 // Flags for u-root builder. 41 var ( 42 build, format, tmpDir, base, outputPath *string 43 uinitCmd, initCmd *string 44 defaultShell *string 45 useExistingInit *bool 46 noCommands *bool 47 extraFiles multiFlag 48 statsOutputPath *string 49 statsLabel *string 50 shellbang *bool 51 tags *string 52 // For the new gobusybox support 53 usegobusybox *bool 54 genDir *string 55 // For the new "filepath only" logic 56 urootSourceDir *string 57 ) 58 59 func init() { 60 var sh string 61 switch gbbgolang.Default().GOOS { 62 case "plan9": 63 sh = "" 64 default: 65 sh = "elvish" 66 } 67 68 build = flag.String("build", "gbb", "u-root build format (e.g. bb/gbb or binary).") 69 format = flag.String("format", "cpio", "Archival format.") 70 71 tmpDir = flag.String("tmpdir", "", "Temporary directory to put binaries in.") 72 73 base = flag.String("base", "", "Base archive to add files to. By default, this is a couple of directories like /bin, /etc, etc. u-root has a default internally supplied set of files; use base=/dev/null if you don't want any base files.") 74 useExistingInit = flag.Bool("useinit", false, "Use existing init from base archive (only if --base was specified).") 75 outputPath = flag.String("o", "", "Path to output initramfs file.") 76 77 initCmd = flag.String("initcmd", "init", "Symlink target for /init. Can be an absolute path or a u-root command name. Use initcmd=\"\" if you don't want the symlink.") 78 uinitCmd = flag.String("uinitcmd", "", "Symlink target and arguments for /bin/uinit. Can be an absolute path or a u-root command name. Use uinitcmd=\"\" if you don't want the symlink. E.g. -uinitcmd=\"echo foobar\"") 79 defaultShell = flag.String("defaultsh", sh, "Default shell. Can be an absolute path or a u-root command name. Use defaultsh=\"\" if you don't want the symlink.") 80 81 noCommands = flag.Bool("nocmd", false, "Build no Go commands; initramfs only") 82 83 flag.Var(&extraFiles, "files", "Additional files, directories, and binaries (with their ldd dependencies) to add to archive. Can be speficified multiple times.") 84 85 shellbang = flag.Bool("shellbang", false, "Use #! instead of symlinks for busybox") 86 87 statsOutputPath = flag.String("stats-output-path", "", "Write build stats to this file (JSON)") 88 statsLabel = flag.String("stats-label", "", "Use this statsLabel when writing stats") 89 90 tags = flag.String("tags", "", "Comma separated list of build tags") 91 92 // Flags for the gobusybox, which we hope to move to, since it works with modules. 93 genDir = flag.String("gen-dir", "", "Directory to generate source in") 94 95 // Flag for the new filepath only mode. This will be required to find the u-root commands and make templates work 96 // In almost every case, "." is fine. 97 urootSourceDir = flag.String("uroot-source", ".", "Path to the locally checked out u-root source tree in case commands from there are desired.") 98 } 99 100 type buildStats struct { 101 Label string `json:"label,omitempty"` 102 Time int64 `json:"time"` 103 Duration float64 `json:"duration"` 104 OutputSize int64 `json:"output_size"` 105 } 106 107 func writeBuildStats(stats buildStats, path string) error { 108 var allStats []buildStats 109 if data, err := os.ReadFile(*statsOutputPath); err == nil { 110 json.Unmarshal(data, &allStats) 111 } 112 found := false 113 for i, s := range allStats { 114 if s.Label == stats.Label { 115 allStats[i] = stats 116 found = true 117 break 118 } 119 } 120 if !found { 121 allStats = append(allStats, stats) 122 sort.Slice(allStats, func(i, j int) bool { 123 return strings.Compare(allStats[i].Label, allStats[j].Label) == -1 124 }) 125 } 126 data, err := json.MarshalIndent(allStats, "", " ") 127 if err != nil { 128 return err 129 } 130 if err := os.WriteFile(*statsOutputPath, data, 0o644); err != nil { 131 return err 132 } 133 return nil 134 } 135 136 func generateLabel(env gbbgolang.Environ) string { 137 var baseCmds []string 138 if len(flag.Args()) > 0 { 139 // Use the last component of the name to keep the label short 140 for _, e := range flag.Args() { 141 baseCmds = append(baseCmds, path.Base(e)) 142 } 143 } else { 144 baseCmds = []string{"core"} 145 } 146 return fmt.Sprintf("%s-%s-%s-%s", *build, env.GOOS, env.GOARCH, strings.Join(baseCmds, "_")) 147 } 148 149 func main() { 150 gbbOpts := &gbbgolang.BuildOpts{} 151 gbbOpts.RegisterFlags(flag.CommandLine) 152 153 l := log.New(os.Stderr, "", log.Ltime) 154 155 // Register an alias for -go-no-strip for backwards compatibility. 156 flag.CommandLine.BoolVar(&gbbOpts.NoStrip, "no-strip", false, "Build unstripped binaries") 157 flag.Parse() 158 159 if usrc := os.Getenv("UROOT_SOURCE"); usrc != "" && *urootSourceDir == "" { 160 *urootSourceDir = usrc 161 } 162 163 env := gbbgolang.Default() 164 env.BuildTags = strings.Split(*tags, ",") 165 if env.CgoEnabled { 166 l.Printf("Disabling CGO for u-root...") 167 env.CgoEnabled = false 168 } 169 l.Printf("Build environment: %s", env) 170 if env.GOOS != "linux" { 171 l.Printf("GOOS is not linux. Did you mean to set GOOS=linux?") 172 } 173 174 start := time.Now() 175 176 // Main is in a separate functions so defers run on return. 177 if err := Main(l, env, gbbOpts); err != nil { 178 l.Fatalf("Build error: %v", err) 179 } 180 181 elapsed := time.Now().Sub(start) 182 183 stats := buildStats{ 184 Label: *statsLabel, 185 Time: start.Unix(), 186 Duration: float64(elapsed.Milliseconds()) / 1000, 187 } 188 if stats.Label == "" { 189 stats.Label = generateLabel(env) 190 } 191 if stat, err := os.Stat(*outputPath); err == nil && stat.ModTime().After(start) { 192 l.Printf("Successfully built %q (size %d).", *outputPath, stat.Size()) 193 stats.OutputSize = stat.Size() 194 if *statsOutputPath != "" { 195 if err := writeBuildStats(stats, *statsOutputPath); err == nil { 196 l.Printf("Wrote stats to %q (label %q)", *statsOutputPath, stats.Label) 197 } else { 198 l.Printf("Failed to write stats to %s: %v", *statsOutputPath, err) 199 } 200 } 201 } 202 } 203 204 var recommendedVersions = []string{ 205 "go1.19", 206 "go1.20", 207 } 208 209 func isRecommendedVersion(v string) bool { 210 for _, r := range recommendedVersions { 211 if strings.HasPrefix(v, r) { 212 return true 213 } 214 } 215 return false 216 } 217 218 func canFindSource(dir string) error { 219 d := filepath.Join(dir, "cmds", "core") 220 if _, err := os.Stat(d); err != nil { 221 return fmt.Errorf("can not build u-root in %q:%w (-uroot-source may be incorrect or not set)", *urootSourceDir, os.ErrNotExist) 222 } 223 return nil 224 } 225 226 // Main is a separate function so defers are run on return, which they wouldn't 227 // on exit. 228 func Main(l ulog.Logger, env gbbgolang.Environ, buildOpts *gbbgolang.BuildOpts) error { 229 v, err := env.Version() 230 if err != nil { 231 l.Printf("Could not get environment's Go version, using runtime's version: %v", err) 232 v = runtime.Version() 233 } 234 if !isRecommendedVersion(v) { 235 l.Printf(`WARNING: You are not using one of the recommended Go versions (have = %s, recommended = %v). 236 Some packages may not compile. 237 Go to https://golang.org/doc/install to find out how to install a newer version of Go, 238 or use https://godoc.org/golang.org/dl/%s to install an additional version of Go.`, 239 v, recommendedVersions, recommendedVersions[0]) 240 } 241 242 archiver, err := initramfs.GetArchiver(*format) 243 if err != nil { 244 return err 245 } 246 247 // Open the target initramfs file. 248 if *outputPath == "" { 249 if len(env.GOOS) == 0 && len(env.GOARCH) == 0 { 250 return fmt.Errorf("passed no path, GOOS, and GOARCH to CPIOArchiver.OpenWriter") 251 } 252 *outputPath = fmt.Sprintf("/tmp/initramfs.%s_%s.cpio", env.GOOS, env.GOARCH) 253 } 254 w, err := archiver.OpenWriter(l, *outputPath) 255 if err != nil { 256 return err 257 } 258 259 var baseFile initramfs.Reader 260 if *base != "" { 261 bf, err := os.Open(*base) 262 if err != nil { 263 return err 264 } 265 defer bf.Close() 266 baseFile = archiver.Reader(bf) 267 } else { 268 baseFile = uroot.DefaultRamfs().Reader() 269 } 270 271 tempDir := *tmpDir 272 if tempDir == "" { 273 var err error 274 tempDir, err = os.MkdirTemp("", "u-root") 275 if err != nil { 276 return err 277 } 278 defer os.RemoveAll(tempDir) 279 } else if _, err := os.Stat(tempDir); os.IsNotExist(err) { 280 if err := os.MkdirAll(tempDir, 0o755); err != nil { 281 return fmt.Errorf("temporary directory %q did not exist; tried to mkdir but failed: %v", tempDir, err) 282 } 283 } 284 285 var ( 286 c []uroot.Commands 287 initCommand = *initCmd 288 ) 289 if !*noCommands { 290 var b builder.Builder 291 switch *build { 292 case "bb", "gbb": 293 l.Printf("NOTE: building with the new gobusybox; to get the old behavior check out commit 8b790de") 294 b = builder.GBBBuilder{ShellBang: *shellbang} 295 case "binary": 296 b = builder.BinaryBuilder{} 297 case "source": 298 return fmt.Errorf("source mode has been deprecated") 299 default: 300 return fmt.Errorf("could not find builder %q", *build) 301 } 302 303 // Resolve globs into package imports. 304 // 305 // Currently allowed format: 306 // Paths to Go package directories; e.g. $GOPATH/src/github.com/mvdan/u-root-coreutils/cmds/* 307 // u-root templates; e.g. all, core, minimal (requires uroot-source be valid) 308 // Import paths of u-root commands; e.g. github.com/mvdan/u-root-coreutils/cmds/* (requires uroot-source) 309 var pkgs []string 310 for _, a := range flag.Args() { 311 p, ok := templates[a] 312 if !ok { 313 if !validateArg(a) { 314 l.Printf("%q is not a valid path, allowed are only existing relative or absolute file paths!", a) 315 continue 316 } 317 pkgs = append(pkgs, a) 318 continue 319 } 320 pkgs = append(pkgs, p...) 321 } 322 if len(pkgs) == 0 { 323 pkgs = []string{"github.com/mvdan/u-root-coreutils/cmds/core/*"} 324 } 325 326 // The command-line tool only allows specifying one build mode 327 // right now. 328 c = append(c, uroot.Commands{ 329 Builder: b, 330 Packages: pkgs, 331 }) 332 } 333 334 opts := uroot.Opts{ 335 Env: &env, 336 Commands: c, 337 UrootSource: *urootSourceDir, 338 TempDir: tempDir, 339 ExtraFiles: extraFiles, 340 OutputFile: w, 341 BaseArchive: baseFile, 342 UseExistingInit: *useExistingInit, 343 InitCmd: initCommand, 344 DefaultShell: *defaultShell, 345 BuildOpts: buildOpts, 346 } 347 uinitArgs := shlex.Argv(*uinitCmd) 348 if len(uinitArgs) > 0 { 349 opts.UinitCmd = uinitArgs[0] 350 } 351 if len(uinitArgs) > 1 { 352 opts.UinitArgs = uinitArgs[1:] 353 } 354 return uroot.CreateInitramfs(l, opts) 355 } 356 357 func validateArg(arg string) bool { 358 // Do the simple thing first: stat the path. 359 // This saves incorrect diagnostics when the 360 // path is a perfectly valid relative path. 361 if _, err := os.Stat(arg); err == nil { 362 return true 363 } 364 if !checkPrefix(arg) { 365 paths, err := filepath.Glob(arg) 366 if err != nil { 367 return false 368 } 369 for _, path := range paths { 370 if !checkPrefix(path) { 371 return false 372 } 373 } 374 } 375 376 return true 377 } 378 379 func checkPrefix(arg string) bool { 380 prefixes := []string{".", "/", "-", "cmds", "github.com/mvdan/u-root-coreutils"} 381 for _, prefix := range prefixes { 382 if strings.HasPrefix(arg, prefix) { 383 return true 384 } 385 } 386 387 return false 388 }