github.com/mvdan/u-root-coreutils@v0.0.0-20230122170626-c2eef2898555/pkg/uroot/uroot.go (about) 1 // Copyright 2015-2017 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 uroot creates root file systems from Go programs. 6 // 7 // uroot will appropriately compile the Go programs, create symlinks for their 8 // names, and assemble an initramfs with additional files as specified. 9 package uroot 10 11 import ( 12 "debug/elf" 13 "fmt" 14 "os" 15 "path" 16 "path/filepath" 17 "strings" 18 19 "github.com/u-root/gobusybox/src/pkg/bb/findpkg" 20 gbbgolang "github.com/u-root/gobusybox/src/pkg/golang" 21 "github.com/mvdan/u-root-coreutils/pkg/cpio" 22 "github.com/mvdan/u-root-coreutils/pkg/ldd" 23 "github.com/mvdan/u-root-coreutils/pkg/uflag" 24 "github.com/mvdan/u-root-coreutils/pkg/ulog" 25 "github.com/mvdan/u-root-coreutils/pkg/uroot/builder" 26 "github.com/mvdan/u-root-coreutils/pkg/uroot/initramfs" 27 ) 28 29 // These constants are used in DefaultRamfs. 30 const ( 31 // This is the literal timezone file for GMT-0. Given that we have no 32 // idea where we will be running, GMT seems a reasonable guess. If it 33 // matters, setup code should download and change this to something 34 // else. 35 gmt0 = "TZif2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00GMT\x00\x00\x00TZif2\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x01\x00\x00\x00\x04\xf8\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00GMT\x00\x00\x00\nGMT0\n" 36 37 nameserver = "nameserver 8.8.8.8\n" 38 ) 39 40 // DefaultRamRamfs returns a cpio.Archive for the target OS. 41 // If an OS is not known it will return a reasonable u-root specific 42 // default. 43 func DefaultRamfs() *cpio.Archive { 44 switch gbbgolang.Default().GOOS { 45 case "linux": 46 return cpio.ArchiveFromRecords([]cpio.Record{ 47 cpio.Directory("bin", 0o755), 48 cpio.Directory("dev", 0o755), 49 cpio.Directory("env", 0o755), 50 cpio.Directory("etc", 0o755), 51 cpio.Directory("lib64", 0o755), 52 cpio.Directory("proc", 0o755), 53 cpio.Directory("sys", 0o755), 54 cpio.Directory("tcz", 0o755), 55 cpio.Directory("tmp", 0o777), 56 cpio.Directory("ubin", 0o755), 57 cpio.Directory("usr", 0o755), 58 cpio.Directory("usr/lib", 0o755), 59 cpio.Directory("var/log", 0o777), 60 cpio.CharDev("dev/console", 0o600, 5, 1), 61 cpio.CharDev("dev/tty", 0o666, 5, 0), 62 cpio.CharDev("dev/null", 0o666, 1, 3), 63 cpio.CharDev("dev/port", 0o640, 1, 4), 64 cpio.CharDev("dev/urandom", 0o666, 1, 9), 65 cpio.StaticFile("etc/resolv.conf", nameserver, 0o644), 66 cpio.StaticFile("etc/localtime", gmt0, 0o644), 67 }) 68 default: 69 return cpio.ArchiveFromRecords([]cpio.Record{ 70 cpio.Directory("ubin", 0o755), 71 cpio.Directory("bbin", 0o755), 72 }) 73 } 74 } 75 76 // Commands specifies a list of Golang packages to build with a builder, e.g. 77 // in busybox mode, source mode, or binary mode. 78 // 79 // See Builder for an explanation of build modes. 80 type Commands struct { 81 // Builder is the Go compiler mode. 82 Builder builder.Builder 83 84 // Packages are the Go commands to include (compiled or otherwise) and 85 // add to the archive. 86 // 87 // Currently allowed formats: 88 // 89 // - package imports; e.g. github.com/mvdan/u-root-coreutils/cmds/ls 90 // - globs of package imports; e.g. github.com/mvdan/u-root-coreutils/cmds/* 91 // - paths to package directories; e.g. $GOPATH/src/github.com/mvdan/u-root-coreutils/cmds/ls 92 // - globs of paths to package directories; e.g. ./cmds/* 93 // 94 // Directories may be relative or absolute, with or without globs. 95 // Globs are resolved using filepath.Glob. 96 Packages []string 97 98 // BinaryDir is the directory in which the resulting binaries are 99 // placed inside the initramfs. 100 // 101 // BinaryDir may be empty, in which case Builder.DefaultBinaryDir() 102 // will be used. 103 BinaryDir string 104 } 105 106 // TargetDir returns the initramfs binary directory for these Commands. 107 func (c Commands) TargetDir() string { 108 if len(c.BinaryDir) != 0 { 109 return c.BinaryDir 110 } 111 return c.Builder.DefaultBinaryDir() 112 } 113 114 // Opts are the arguments to CreateInitramfs. 115 // 116 // Opts contains everything that influences initramfs creation such as the Go 117 // build environment. 118 type Opts struct { 119 // Env is the Golang build environment (GOOS, GOARCH, etc). 120 // 121 // If nil, gbbgolang.Default is used. 122 Env *gbbgolang.Environ 123 124 // Commands specify packages to build using a specific builder. 125 // 126 // E.g. the following will build 'ls' and 'ip' in busybox mode, but 127 // 'cd' and 'cat' as separate binaries. 'cd', 'cat', 'bb', and symlinks 128 // from 'ls' and 'ip' will be added to the final initramfs. 129 // 130 // []Commands{ 131 // Commands{ 132 // Builder: builder.BusyBox, 133 // Packages: []string{ 134 // "github.com/mvdan/u-root-coreutils/cmds/ls", 135 // "github.com/mvdan/u-root-coreutils/cmds/ip", 136 // }, 137 // }, 138 // Commands{ 139 // Builder: builder.Binary, 140 // Packages: []string{ 141 // "github.com/mvdan/u-root-coreutils/cmds/cd", 142 // "github.com/mvdan/u-root-coreutils/cmds/cat", 143 // }, 144 // }, 145 // } 146 Commands []Commands 147 148 // UrootSource is the filesystem path to the locally checked out 149 // u-root source tree. This is needed to resolve templates or 150 // import paths of u-root commands. 151 UrootSource string 152 153 // TempDir is a temporary directory for builders to store files in. 154 TempDir string 155 156 // ExtraFiles are files to add to the archive in addition to the Go 157 // packages. 158 // 159 // Shared library dependencies will automatically also be added to the 160 // archive using ldd, unless SkipLDD (below) is true. 161 // 162 // The following formats are allowed in the list: 163 // 164 // - "/home/chrisko/foo:root/bar" adds the file from absolute path 165 // /home/chrisko/foo on the host at the relative root/bar in the 166 // archive. 167 // - "/home/foo" is equivalent to "/home/foo:home/foo". 168 ExtraFiles []string 169 170 // If true, do not use ldd to pick up dependencies from local machine for 171 // ExtraFiles. Useful if you have all deps revision controlled and wish to 172 // ensure builds are repeatable, and/or if the local machine's binaries use 173 // instructions unavailable on the emulated cpu. 174 // 175 // If you turn this on but do not manually list all deps, affected binaries 176 // will misbehave. 177 SkipLDD bool 178 179 // OutputFile is the archive output file. 180 OutputFile initramfs.Writer 181 182 // BaseArchive is an existing initramfs to include in the resulting 183 // initramfs. 184 BaseArchive initramfs.Reader 185 186 // UseExistingInit determines whether the existing init from 187 // BaseArchive should be used. 188 // 189 // If this is false, the "init" from BaseArchive will be renamed to 190 // "inito" (init-original). 191 UseExistingInit bool 192 193 // InitCmd is the name of a command to link /init to. 194 // 195 // This can be an absolute path or the name of a command included in 196 // Commands. 197 // 198 // If this is empty, no init symlink will be created, but a user may 199 // still specify a command called init or include an /init file. 200 InitCmd string 201 202 // UinitCmd is the name of a command to link /bin/uinit to. 203 // 204 // This can be an absolute path or the name of a command included in 205 // Commands. 206 // 207 // The u-root init will always attempt to fork/exec a uinit program, 208 // and append arguments from both the kernel command-line 209 // (uroot.uinitargs) as well as specified in UinitArgs. 210 // 211 // If this is empty, no uinit symlink will be created, but a user may 212 // still specify a command called uinit or include a /bin/uinit file. 213 UinitCmd string 214 215 // UinitArgs are the arguments passed to /bin/uinit. 216 UinitArgs []string 217 218 // DefaultShell is the default shell to start after init. 219 // 220 // This can be an absolute path or the name of a command included in 221 // Commands. 222 // 223 // This must be specified to have a default shell. 224 DefaultShell string 225 226 // Build options for building go binaries. Ultimate this holds all the 227 // args that end up being passed to `go build`. 228 BuildOpts *gbbgolang.BuildOpts 229 } 230 231 // CreateInitramfs creates an initramfs built to opts' specifications. 232 func CreateInitramfs(logger ulog.Logger, opts Opts) error { 233 if _, err := os.Stat(opts.TempDir); os.IsNotExist(err) { 234 return fmt.Errorf("temp dir %q must exist: %v", opts.TempDir, err) 235 } 236 if opts.OutputFile == nil { 237 return fmt.Errorf("must give output file") 238 } 239 240 env := gbbgolang.Default() 241 if opts.Env != nil { 242 env = *opts.Env 243 } 244 if opts.BuildOpts == nil { 245 opts.BuildOpts = &gbbgolang.BuildOpts{} 246 } 247 248 files := initramfs.NewFiles() 249 250 lookupEnv := findpkg.DefaultEnv() 251 if opts.UrootSource != "" { 252 lookupEnv.URootSource = opts.UrootSource 253 } 254 255 // Expand commands. 256 for index, cmds := range opts.Commands { 257 paths, err := findpkg.ResolveGlobs(logger, env, lookupEnv, cmds.Packages) 258 if err != nil { 259 return err 260 } 261 opts.Commands[index].Packages = paths 262 } 263 264 // Add each build mode's commands to the archive. 265 for _, cmds := range opts.Commands { 266 builderTmpDir, err := os.MkdirTemp(opts.TempDir, "builder") 267 if err != nil { 268 return err 269 } 270 271 // Build packages. 272 bOpts := builder.Opts{ 273 Env: env, 274 BuildOpts: opts.BuildOpts, 275 Packages: cmds.Packages, 276 TempDir: builderTmpDir, 277 BinaryDir: cmds.TargetDir(), 278 } 279 if err := cmds.Builder.Build(logger, files, bOpts); err != nil { 280 return fmt.Errorf("error building: %v", err) 281 } 282 } 283 284 // Open the target initramfs file. 285 archive := &initramfs.Opts{ 286 Files: files, 287 OutputFile: opts.OutputFile, 288 BaseArchive: opts.BaseArchive, 289 UseExistingInit: opts.UseExistingInit, 290 } 291 if err := ParseExtraFiles(logger, archive.Files, opts.ExtraFiles, !opts.SkipLDD); err != nil { 292 return err 293 } 294 if err := opts.addSymlinkTo(logger, archive, opts.UinitCmd, "bin/uinit"); err != nil { 295 return fmt.Errorf("%v: specify -uinitcmd=\"\" to ignore this error and build without a uinit", err) 296 } 297 if len(opts.UinitArgs) > 0 { 298 if err := archive.AddRecord(cpio.StaticFile("etc/uinit.flags", uflag.ArgvToFile(opts.UinitArgs), 0o444)); err != nil { 299 return fmt.Errorf("%v: could not add uinit arguments from UinitArgs (-uinitcmd) to initramfs", err) 300 } 301 } 302 if err := opts.addSymlinkTo(logger, archive, opts.InitCmd, "init"); err != nil { 303 return fmt.Errorf("%v: specify -initcmd=\"\" to ignore this error and build without an init (or, did you specify a list, and are you missing github.com/mvdan/u-root-coreutils/cmds/core/init?)", err) 304 } 305 if err := opts.addSymlinkTo(logger, archive, opts.DefaultShell, "bin/sh"); err != nil { 306 return fmt.Errorf("%v: specify -defaultsh=\"\" to ignore this error and build without a shell", err) 307 } 308 if err := opts.addSymlinkTo(logger, archive, opts.DefaultShell, "bin/defaultsh"); err != nil { 309 return fmt.Errorf("%v: specify -defaultsh=\"\" to ignore this error and build without a shell", err) 310 } 311 312 // Finally, write the archive. 313 if err := initramfs.Write(archive); err != nil { 314 return fmt.Errorf("error archiving: %v", err) 315 } 316 return nil 317 } 318 319 func (o *Opts) addSymlinkTo(logger ulog.Logger, archive *initramfs.Opts, command string, source string) error { 320 if len(command) == 0 { 321 return nil 322 } 323 324 target, err := resolveCommandOrPath(command, o.Commands) 325 if err != nil { 326 if o.Commands != nil { 327 return fmt.Errorf("could not create symlink from %q to %q: %v", source, command, err) 328 } 329 logger.Printf("Could not create symlink from %q to %q: %v", source, command, err) 330 return nil 331 } 332 333 // Make a relative symlink from /source -> target 334 // 335 // E.g. bin/defaultsh -> target, so you need to 336 // filepath.Rel(/bin, target) since relative symlinks are 337 // evaluated from their PARENT directory. 338 relTarget, err := filepath.Rel(filepath.Join("/", filepath.Dir(source)), target) 339 if err != nil { 340 return err 341 } 342 343 if err := archive.AddRecord(cpio.Symlink(source, relTarget)); err != nil { 344 return fmt.Errorf("failed to add symlink %s -> %s to initramfs: %v", source, relTarget, err) 345 } 346 return nil 347 } 348 349 func resolveCommandOrPath(cmd string, cmds []Commands) (string, error) { 350 if strings.ContainsRune(cmd, filepath.Separator) { 351 return cmd, nil 352 } 353 354 // Each build mode has its own binary dir (/bbin or /bin or /ubin). 355 // 356 // Figure out which build mode the shell is in, and symlink to that 357 // build mode. 358 for _, c := range cmds { 359 for _, p := range c.Packages { 360 if name := path.Base(p); name == cmd { 361 return path.Join("/", c.TargetDir(), cmd), nil 362 } 363 } 364 } 365 366 return "", fmt.Errorf("command or path %q not included in u-root build", cmd) 367 } 368 369 // ParseExtraFiles adds files from the extraFiles list to the archive. 370 // 371 // The following formats are allowed in the extraFiles list: 372 // 373 // - "/home/chrisko/foo:root/bar" adds the file from absolute path 374 // /home/chrisko/foo on the host at the relative root/bar in the 375 // archive. 376 // - "/home/foo" is equivalent to "/home/foo:home/foo". 377 // 378 // ParseExtraFiles will also add ldd-listed dependencies if lddDeps is true. 379 func ParseExtraFiles(logger ulog.Logger, archive *initramfs.Files, extraFiles []string, lddDeps bool) error { 380 var err error 381 // Add files from command line. 382 for _, file := range extraFiles { 383 var src, dst string 384 parts := strings.SplitN(file, ":", 2) 385 if len(parts) == 2 { 386 // treat the entry with the new src:dst syntax 387 src = filepath.Clean(parts[0]) 388 dst = filepath.Clean(parts[1]) 389 } else { 390 // plain old syntax 391 // filepath.Clean interprets an empty string as CWD for no good reason. 392 if len(file) == 0 { 393 continue 394 } 395 src = filepath.Clean(file) 396 dst = src 397 if filepath.IsAbs(dst) { 398 dst, err = filepath.Rel("/", dst) 399 if err != nil { 400 return fmt.Errorf("cannot make path relative to /: %v: %v", dst, err) 401 } 402 } 403 } 404 src, err := filepath.Abs(src) 405 if err != nil { 406 return fmt.Errorf("couldn't find absolute path for %q: %v", src, err) 407 } 408 if err := archive.AddFileNoFollow(src, dst); err != nil { 409 return fmt.Errorf("couldn't add %q to archive: %v", file, err) 410 } 411 412 if lddDeps { 413 // Users are frequently naming directories now, not just files. 414 // Hence we must use walk here, not just check the one file. 415 if err := filepath.Walk(src, func(name string, info os.FileInfo, err error) error { 416 if err != nil { 417 return err 418 } 419 if info.IsDir() { 420 return nil 421 } 422 // Try to open it as an ELF. If that fails, we can skip the ldd 423 // step. The file will still be included from above. 424 f, err := elf.Open(name) 425 if err != nil { 426 return nil 427 } 428 if err = f.Close(); err != nil { 429 logger.Printf("WARNING: Closing ELF file %q: %v", name, err) 430 } 431 // Pull dependencies in the case of binaries. If `path` is not 432 // a binary, `libs` will just be empty. 433 libs, err := ldd.List([]string{name}) 434 if err != nil { 435 return fmt.Errorf("WARNING: couldn't add ldd dependencies for %q: %v", name, err) 436 } 437 for _, lib := range libs { 438 // N.B.: we already added information about the src. 439 // Don't add it twice. We have to do this check here in 440 // case we're renaming the src to a different dest. 441 if lib == name { 442 continue 443 } 444 if err := archive.AddFileNoFollow(lib, lib[1:]); err != nil { 445 logger.Printf("WARNING: couldn't add ldd dependencies for %q: %v", lib, err) 446 } 447 } 448 return nil 449 }); err != nil { 450 logger.Printf("Getting dependencies for %q: %v", src, err) 451 } 452 } 453 } 454 return nil 455 } 456 457 // AddCommands adds commands to the build. 458 func (o *Opts) AddCommands(c ...Commands) { 459 o.Commands = append(o.Commands, c...) 460 } 461 462 func (o *Opts) AddBusyBoxCommands(pkgs ...string) { 463 for i, cmds := range o.Commands { 464 if cmds.Builder == builder.BusyBox { 465 o.Commands[i].Packages = append(cmds.Packages, pkgs...) 466 return 467 } 468 } 469 470 // Not found? Add first busybox. 471 o.AddCommands(BusyBoxCmds(pkgs...)...) 472 } 473 474 // BinaryCmds returns a list of Commands with cmds built as a busybox. 475 func BinaryCmds(cmds ...string) []Commands { 476 if len(cmds) == 0 { 477 return nil 478 } 479 return []Commands{ 480 { 481 Builder: builder.Binary, 482 Packages: cmds, 483 }, 484 } 485 } 486 487 // BusyBoxCmds returns a list of Commands with cmds built as a busybox. 488 func BusyBoxCmds(cmds ...string) []Commands { 489 if len(cmds) == 0 { 490 return nil 491 } 492 return []Commands{ 493 { 494 Builder: builder.BusyBox, 495 Packages: cmds, 496 }, 497 } 498 }