├── .github └── workflows │ ├── go.yml │ └── golangci-lint.yml ├── .golangci.yml ├── .revive.toml ├── LICENSE ├── README.md ├── cmd ├── gentpldeps │ ├── .gitignore │ └── main.go └── mkuimage │ ├── .gitignore │ ├── main.go │ ├── main_test.go │ └── testdata │ └── test-config.yaml ├── cpio ├── archive.go ├── archive_test.go ├── const.go ├── cpio.go ├── fs_plan9.go ├── fs_unix.go ├── fs_windows.go ├── internal │ └── upath │ │ ├── safejoin.go │ │ └── safejoin_test.go ├── mknod_freebsd.go ├── mknod_unix.go ├── newc.go ├── newc_test.go ├── sysinfo_darwin.go ├── sysinfo_freebsd.go ├── sysinfo_linux.go ├── sysinfo_plan9.go ├── sysinfo_windows.go ├── testdata │ └── fuzz │ │ ├── corpora │ │ ├── archive1.cpio │ │ ├── archive2.cpio │ │ ├── archive3.cpio │ │ ├── archive4.cpio │ │ └── archive5.cpio │ │ ├── cpio.dict │ │ └── fuzz_read_write_newc.options ├── utils.go └── utils_test.go ├── cross-compile.sh ├── dependencies.go ├── fileflag ├── flagfile.go └── flagfile_test.go ├── go.mod ├── go.sum ├── ldd ├── ldd.go ├── ldd_other.go ├── ldd_test.go ├── ldd_unix.go ├── ldd_unix_test.go ├── ldso_freebsd.go └── ldso_linux.go └── uimage ├── builder ├── binary.go ├── binary_test.go ├── builder.go ├── gbb.go └── gbb_test.go ├── initramfs ├── archive.go ├── cpio.go ├── dir.go ├── files.go ├── files_test.go └── test │ └── ramfs.go ├── mkuimage ├── cmd.go ├── cmd_test.go ├── uflags.go └── uflags_test.go ├── templates ├── templates.go └── templates_test.go ├── uimage.go └── uimage_test.go /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | name: Go 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | schedule: 9 | # Every day at 8am. 10 | - cron: "0 8 * * *" 11 | 12 | # Cancel running workflows on new push to a PR. 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} 15 | cancel-in-progress: true 16 | 17 | jobs: 18 | cross_build: 19 | name: Cross-platform builds 20 | strategy: 21 | matrix: 22 | go-version: ['1.21.x', '1.22.x'] 23 | runs-on: ubuntu-latest 24 | steps: 25 | - uses: actions/checkout@v4 26 | - name: Setup Go ${{ matrix.go-version }} 27 | uses: actions/setup-go@v4 28 | with: 29 | go-version: ${{ matrix.go-version }} 30 | 31 | - name: Cross-compile 32 | run: ./cross-compile.sh 33 | 34 | build_and_test: 35 | name: Build and test 36 | strategy: 37 | matrix: 38 | go-version: ['1.21.x', '1.22.x'] 39 | platform: [ubuntu-latest] 40 | 41 | runs-on: ${{ matrix.platform }} 42 | steps: 43 | - uses: actions/checkout@v4 44 | - name: Setup Go ${{ matrix.go-version }} 45 | uses: actions/setup-go@v4 46 | with: 47 | go-version: ${{ matrix.go-version }} 48 | 49 | - name: Build 50 | run: go build -v ./... 51 | 52 | - name: Test 53 | run: go test -v -covermode atomic -coverpkg ./... -coverprofile cover.out ./... 54 | 55 | - name: Convert GOCOVERDIR coverage data 56 | run: go tool covdata textfmt -i=cmd/mkuimage/cover -o cmdcover.out 57 | 58 | - uses: codecov/codecov-action@v4-beta 59 | env: 60 | CODECOV_TOKEN: '804457b0-03f8-4cf6-bc99-eaf43399177b' 61 | with: 62 | flags: ${{ matrix.platform }}-unit 63 | fail_ci_if_error: true 64 | verbose: true 65 | files: cover.out,cmdcover.out 66 | 67 | race: 68 | name: Race test 69 | strategy: 70 | matrix: 71 | go-version: ['1.21.x', '1.22.x'] 72 | platform: [ubuntu-latest] 73 | 74 | runs-on: ${{ matrix.platform }} 75 | steps: 76 | - uses: actions/checkout@v4 77 | - name: Setup Go ${{ matrix.go-version }} 78 | uses: actions/setup-go@v4 79 | with: 80 | go-version: ${{ matrix.go-version }} 81 | 82 | - name: Race 83 | run: go test -race -v ./... 84 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | permissions: 13 | contents: read 14 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 15 | pull-requests: read 16 | 17 | jobs: 18 | golangci: 19 | name: lint 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-go@v4 24 | with: 25 | go-version: '1.22.x' 26 | - name: golangci-lint 27 | uses: golangci/golangci-lint-action@v3 28 | with: 29 | version: latest 30 | only-new-issues: true 31 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - containedctx 4 | - gocritic 5 | - godot 6 | - nilerr 7 | - revive 8 | - thelper 9 | - unconvert 10 | 11 | issues: 12 | include: 13 | - EXC0012 14 | - EXC0013 15 | - EXC0014 16 | - EXC0015 17 | linters-settings: 18 | revive: 19 | # Maximum number of open files at the same time. 20 | # See https://github.com/mgechev/revive#command-line-flags 21 | # Defaults to unlimited. 22 | max-open-files: 2048 23 | # When set to false, ignores files with "GENERATED" header, similar to golint. 24 | # See https://github.com/mgechev/revive#available-rules for details. 25 | # Default: false 26 | ignore-generated-header: true 27 | # Sets the default severity. 28 | # See https://github.com/mgechev/revive#configuration 29 | # Default: warning 30 | severity: error 31 | # Default: false 32 | # Sets the default failure confidence. 33 | # This means that linting errors with less than 0.8 confidence will be ignored. 34 | # Default: 0.8 35 | confidence: 0.8 36 | rules: 37 | - name: blank-imports 38 | - name: context-as-argument 39 | arguments: 40 | - allowTypesBefore: "*testing.T,*github.com/user/repo/testing.Harness" 41 | - name: context-keys-type 42 | - name: error-return 43 | - name: error-strings 44 | - name: error-naming 45 | - name: exported 46 | arguments: 47 | - "checkPrivateReceivers" 48 | - "sayRepetitiveInsteadOfStutters" 49 | - name: if-return 50 | - name: increment-decrement 51 | - name: var-naming 52 | - name: var-declaration 53 | - name: package-comments 54 | - name: range 55 | - name: receiver-naming 56 | - name: time-naming 57 | - name: unexported-return 58 | - name: indent-error-flow 59 | - name: errorf 60 | 61 | - name: early-return 62 | - name: file-header 63 | arguments: 64 | - "Copyright 20[0-2][0-9](-20[1-2][0-9])? the u-root Authors. All rights reserved Use of this source code is governed by a BSD-style license that can be found in the LICENSE file." 65 | -------------------------------------------------------------------------------- /.revive.toml: -------------------------------------------------------------------------------- 1 | ignoreGeneratedHeader = false 2 | severity = "warning" 3 | confidence = 0.8 4 | errorCode = 0 5 | warningCode = 0 6 | 7 | [rule.blank-imports] 8 | [rule.context-as-argument] 9 | [rule.context-keys-type] 10 | [rule.dot-imports] 11 | [rule.error-return] 12 | [rule.error-strings] 13 | [rule.error-naming] 14 | [rule.exported] 15 | [rule.if-return] 16 | [rule.increment-decrement] 17 | [rule.var-naming] 18 | [rule.var-declaration] 19 | [rule.package-comments] 20 | [rule.range] 21 | [rule.receiver-naming] 22 | [rule.time-naming] 23 | [rule.unexported-return] 24 | [rule.indent-error-flow] 25 | [rule.errorf] 26 | [rule.early-return] 27 | [rule.file-header] 28 | arguments=["Copyright 20[1-2][0-9](-20[1-2][0-9])? the u-root Authors. All rights reserved Use of this source code is governed by a BSD-style license that can be found in the LICENSE file."] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2012-2024, u-root Authors 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # uimage 2 | 3 | [![GoDoc](https://pkg.go.dev/badge/github.com/u-root/mkuimage)](https://pkg.go.dev/github.com/u-root/mkuimage) 4 | [![codecov](https://codecov.io/gh/u-root/mkuimage/graph/badge.svg?token=5Z9B3OyVYi)](https://codecov.io/gh/u-root/mkuimage) 5 | [![Slack](https://slack.osfw.dev/badge.svg)](https://slack.osfw.dev) 6 | [![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://github.com/u-root/mkuimage/blob/main/LICENSE) 7 | 8 | uimage builds initramfs images composed of arbitrary Go commands and files. 9 | 10 | uimage optimizes for space by utilizing 11 | [gobusybox](https://github.com/u-root/gobusybox) to compile many arbitrary Go 12 | commands into one binary. 13 | 14 | uimage can be easily used with [u-root](https://github.com/u-root/u-root), 15 | which contains many Go coreutils-like commands as well as bootloaders. However, 16 | uimage supports compilation of any Go command, and the use of u-root is not 17 | required. 18 | 19 | ## Getting Started 20 | 21 | Make sure your Go version is >= 1.21. If your Go version is lower, 22 | 23 | ```shell 24 | $ go install golang.org/dl/go1.21.5@latest 25 | $ go1.21.5 download 26 | $ go1.21.5 version 27 | # Now use go1.21.5 in place of go 28 | ``` 29 | 30 | Download and install mkuimage either via git: 31 | 32 | ```shell 33 | git clone https://github.com/u-root/mkuimage 34 | cd mkuimage/cmd/mkuimage 35 | go install 36 | ``` 37 | 38 | Or install directly with go: 39 | 40 | ```shell 41 | go install github.com/u-root/mkuimage/cmd/mkuimage@latest 42 | ``` 43 | 44 | > [!NOTE] 45 | > The `mkuimage` command will end up in `$GOPATH/bin/mkuimage`, so you may 46 | > need to add `$GOPATH/bin` to your `$PATH`. 47 | 48 | ## Examples 49 | 50 | Here are some examples of using the `mkuimage` command to build an initramfs. 51 | 52 | ```shell 53 | git clone https://github.com/u-root/u-root 54 | ``` 55 | 56 | Build gobusybox binaries of these two commands and add to initramfs: 57 | 58 | ```shell 59 | $ cd ./u-root 60 | $ mkuimage ./cmds/core/{init,gosh} 61 | 62 | $ cpio -ivt < /tmp/initramfs.linux_amd64.cpio 63 | ... 64 | -rwxr-x--- 0 root root 4542464 Jan 1 1970 bbin/bb 65 | lrwxrwxrwx 0 root root 2 Jan 1 1970 bbin/gosh -> bb 66 | lrwxrwxrwx 0 root root 2 Jan 1 1970 bbin/init -> bb 67 | ... 68 | ``` 69 | 70 | Add symlinks for shell and init: 71 | 72 | ```shell 73 | $ mkuimage -initcmd=init -defaultsh=gosh ./cmds/core/{init,gosh} 74 | 75 | $ cpio -ivt < /tmp/initramfs.linux_amd64.cpio 76 | ... 77 | lrwxrwxrwx 0 root root 12 Jan 1 1970 bin/defaultsh -> ../bbin/gosh 78 | lrwxrwxrwx 0 root root 12 Jan 1 1970 bin/sh -> ../bbin/gosh 79 | ... 80 | lrwxrwxrwx 0 root root 9 Jan 1 1970 init -> bbin/init 81 | ... 82 | ``` 83 | 84 | ### Builds with globs and exclusion 85 | 86 | Build everything from core without ls and losetup: 87 | 88 | ```shell 89 | $ mkuimage ./cmds/core/* -./cmds/core/{ls,losetup} 90 | ``` 91 | 92 | ### Multi-module workspace builds 93 | 94 | > [!IMPORTANT] 95 | > 96 | > `mkuimage` works when `go build` and `go list` work as well. 97 | 98 | There are 2 ways to build multi-module command images using standard Go tooling. 99 | 100 | ```shell 101 | $ mkdir workspace 102 | $ cd workspace 103 | $ git clone https://github.com/u-root/u-root 104 | $ git clone https://github.com/u-root/cpu 105 | 106 | $ go work init ./u-root 107 | $ go work use ./cpu 108 | 109 | $ mkuimage ./u-root/cmds/core/{init,gosh} ./cpu/cmds/cpud 110 | 111 | $ cpio -ivt < /tmp/initramfs.linux_amd64.cpio 112 | ... 113 | -rwxr-x--- 0 root root 6365184 Jan 1 1970 bbin/bb 114 | lrwxrwxrwx 0 root root 2 Jan 1 1970 bbin/cpud -> bb 115 | lrwxrwxrwx 0 root root 2 Jan 1 1970 bbin/gosh -> bb 116 | lrwxrwxrwx 0 root root 2 Jan 1 1970 bbin/init -> bb 117 | ... 118 | 119 | # Works for offline vendored builds as well. 120 | $ go work vendor # Go 1.22 feature. 121 | 122 | $ mkuimage ./u-root/cmds/core/{init,gosh} ./cpu/cmds/cpud 123 | ``` 124 | 125 | `GBB_PATH` is a place that mkuimage will look for commands. Each colon-separated 126 | `GBB_PATH` element is concatenated with patterns from the command-line and 127 | checked for existence. For example: 128 | 129 | ```shell 130 | GBB_PATH=$(pwd)/u-root:$(pwd)/cpu mkuimage \ 131 | cmds/core/{init,gosh} \ 132 | cmds/cpud 133 | 134 | # Matches 135 | # ./u-root/cmds/core/{init,gosh} 136 | # ./cpu/cmds/cpud 137 | ``` 138 | 139 | To ease usability, the `goanywhere` tool can create one Go workspaces the fly. 140 | This works **only with local file system paths**: 141 | 142 | ```shell 143 | $ go install github.com/u-root/gobusybox/src/cmd/goanywhere@latest 144 | 145 | $ goanywhere ./u-root/cmds/core/{init,gosh} ./cpu/cmds/cpud -- go build -o $(pwd) 146 | $ goanywhere ./u-root/cmds/core/{init,gosh} ./cpu/cmds/cpud -- mkuimage 147 | ``` 148 | 149 | `goanywhere` creates a workspace in a temporary directory with the given 150 | modules, and then execs `u-root` in the workspace passing along the command 151 | names. 152 | 153 | `goanywhere` supports `GBB_PATH`, exclusions, globs, and curly brace expansions 154 | as well. 155 | 156 | > [!CAUTION] 157 | > 158 | > While workspaces are good for local compilation, they are not meant to be 159 | > checked in to version control systems. See below for the recommended way. 160 | 161 | ### Multi-module go.mod builds 162 | 163 | You may also create a go.mod with the commands you intend to compile. 164 | 165 | To depend on commands outside of ones own repository, the easiest way to depend 166 | on Go commands is the following: 167 | 168 | ```sh 169 | mkdir mydistro 170 | cd mydistro 171 | go mod init mydistro 172 | ``` 173 | 174 | Create a file with some unused build tag like this to create dependencies on 175 | commands: 176 | 177 | ```go 178 | //go:build tools 179 | 180 | package something 181 | 182 | import ( 183 | _ "github.com/u-root/u-root/cmds/core/ip" 184 | _ "github.com/u-root/u-root/cmds/core/init" 185 | _ "github.com/hugelgupf/p9/cmd/p9ufs" 186 | ) 187 | ``` 188 | 189 | You can generate this file for your repo with the `gencmddeps` tool from 190 | gobusybox: 191 | 192 | ``` 193 | go install github.com/u-root/gobusybox/src/cmd/gencmddeps@latest 194 | 195 | gencmddeps -o deps.go -t tools -p something \ 196 | github.com/u-root/u-root/cmds/core/{ip,init} \ 197 | github.com/hugelgupf/p9/cmd/p9ufs 198 | ``` 199 | 200 | The unused build tag keeps it from being compiled, but its existence forces `go 201 | mod tidy` to add these dependencies to `go.mod`: 202 | 203 | ```sh 204 | go mod tidy 205 | 206 | mkuimage \ 207 | github.com/u-root/u-root/cmds/core/ip \ 208 | github.com/u-root/u-root/cmds/core/init \ 209 | github.com/hugelgupf/p9/cmd/p9ufs 210 | 211 | # Works for offline vendored builds as well. 212 | go mod vendor 213 | 214 | mkuimage \ 215 | github.com/u-root/u-root/cmds/core/ip \ 216 | github.com/u-root/u-root/cmds/core/init \ 217 | github.com/hugelgupf/p9/cmd/p9ufs 218 | ``` 219 | 220 | ## Extra Files 221 | 222 | You may also include additional files in the initramfs using the `-files` flag. 223 | 224 | If you add binaries with `-files` are listed, their ldd dependencies will be 225 | included as well. 226 | 227 | ```shell 228 | $ mkuimage -files /bin/bash 229 | 230 | $ cpio -ivt < /tmp/initramfs.linux_amd64.cpio 231 | ... 232 | -rwxr-xr-x 0 root root 1277936 Jan 1 1970 bin/bash 233 | ... 234 | drwxr-xr-x 0 root root 0 Jan 1 1970 lib/x86_64-linux-gnu 235 | -rwxr-xr-x 0 root root 210792 Jan 1 1970 lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 236 | -rwxr-xr-x 0 root root 1926256 Jan 1 1970 lib/x86_64-linux-gnu/libc.so.6 237 | lrwxrwxrwx 0 root root 15 Jan 1 1970 lib/x86_64-linux-gnu/libtinfo.so.6 -> libtinfo.so.6.4 238 | -rw-r--r-- 0 root root 216368 Jan 1 1970 lib/x86_64-linux-gnu/libtinfo.so.6.4 239 | drwxr-xr-x 0 root root 0 Jan 1 1970 lib64 240 | lrwxrwxrwx 0 root root 42 Jan 1 1970 lib64/ld-linux-x86-64.so.2 -> /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 241 | ... 242 | ``` 243 | 244 | You can determine placement with colons: 245 | 246 | ```shell 247 | $ mkuimage -files "/bin/bash:sbin/sh" 248 | 249 | $ cpio -ivt < /tmp/initramfs.linux_amd64.cpio 250 | ... 251 | -rwxr-xr-x 0 root root 1277936 Jan 1 1970 sbin/sh 252 | ... 253 | ``` 254 | 255 | For example on Debian, if you want to add two kernel modules for testing, 256 | executing your currently booted kernel: 257 | 258 | ```shell 259 | $ mkuimage -files "$HOME/hello.ko:etc/hello.ko" -files "$HOME/hello2.ko:etc/hello2.ko" ./u-root/cmds/core/* 260 | $ qemu-system-x86_64 -kernel /boot/vmlinuz-$(uname -r) -initrd /tmp/initramfs.linux_amd64.cpio 261 | ``` 262 | 263 | ## AMD64 Architecture Level 264 | 265 | Before building for AMD64, verify that the command 266 | 267 | ```shell 268 | go env GOAMD64 269 | ``` 270 | 271 | prints `v1`. A [`GOAMD64` setting](https://go.dev/wiki/MinimumRequirements#amd64) 272 | of any higher version may produce such binaries that don't execute on old AMD64 273 | processors (including the default CPU model of QEMU). 274 | 275 | `GOAMD64` can be reset to `v1` with one of the following methods: 276 | 277 | * through the `GOAMD64` environment variable: 278 | 279 | ```shell 280 | export GOAMD64=v1 281 | ``` 282 | 283 | * through `go env` (only takes effect if the `GOAMD64` environment variable 284 | is not set): 285 | 286 | ```shell 287 | go env -w GOAMD64=v1 288 | ``` 289 | 290 | ## Cross Compilation (targeting different architectures and OSes) 291 | 292 | Just like standard Go tooling, cross compilation is easy and supported. 293 | 294 | To cross compile for an ARM, on Linux: 295 | 296 | ```shell 297 | GOARCH=arm mkuimage ./u-root/cmds/core/* 298 | ``` 299 | 300 | If you are on OS X, and wish to build for Linux on AMD64: 301 | 302 | ```shell 303 | GOOS=linux GOARCH=amd64 ./u-root/cmds/core/* 304 | ``` 305 | 306 | ## Testing in QEMU 307 | 308 | A good way to test the initramfs generated by u-root is with qemu: 309 | 310 | ```shell 311 | qemu-system-x86_64 -nographic -kernel path/to/kernel -initrd /tmp/initramfs.linux_amd64.cpio 312 | ``` 313 | 314 | Note that you do not have to build a special kernel on your own, it is 315 | sufficient to use an existing one. Usually you can find one in `/boot`. 316 | 317 | If you don't have a kernel handy, you can also get the one we use for VM testing: 318 | 319 | ```shell 320 | go install github.com/hugelgupf/vmtest/tools/runvmtest@latest 321 | 322 | runvmtest -- bash -c "cp \$VMTEST_KERNEL ./kernel" 323 | ``` 324 | 325 | It may not have all features you require, however. 326 | 327 | To automate testing, you may use the same 328 | [vmtest](https://github.com/hugelgupf/vmtest) framework that we use as well. It 329 | has native uimage support. 330 | 331 | ## Build Modes 332 | 333 | mkuimage can create an initramfs in two different modes, specified by `-build`: 334 | 335 | * `bb` mode: One busybox-like binary comprising all the Go tools you ask to 336 | include. 337 | See [the gobusybox README for how it works](https://github.com/u-root/gobusybox). 338 | In this mode, mkuimage copies and rewrites the source of the tools you asked 339 | to include to be able to compile everything into one busybox-like binary. 340 | 341 | * `binary` mode: each specified binary is compiled separately and all binaries 342 | are added to the initramfs. 343 | -------------------------------------------------------------------------------- /cmd/gentpldeps/.gitignore: -------------------------------------------------------------------------------- 1 | gentpldeps 2 | -------------------------------------------------------------------------------- /cmd/gentpldeps/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 | // gentpldeps generates a command dependency Go file. 6 | // 7 | // Usage: 8 | // - go get $module... 9 | // - create .mkuimage.yaml file with commands 10 | // - gentpldeps -t -o deps.go -p 11 | package main 12 | 13 | import ( 14 | "bytes" 15 | "flag" 16 | "log" 17 | "os" 18 | "sort" 19 | "text/template" 20 | 21 | "github.com/u-root/gobusybox/src/pkg/bb/findpkg" 22 | "github.com/u-root/gobusybox/src/pkg/golang" 23 | "github.com/u-root/mkuimage/uimage/mkuimage" 24 | "golang.org/x/exp/maps" 25 | ) 26 | 27 | var ( 28 | pkg = flag.String("p", "", "Package") 29 | o = flag.String("o", "", "Output file name") 30 | tag = flag.String("t", "", "Go build tag for the file") 31 | ) 32 | 33 | func main() { 34 | tf := &mkuimage.TemplateFlags{} 35 | tf.RegisterFlags(flag.CommandLine) 36 | flag.Parse() 37 | 38 | if *pkg == "" { 39 | log.Fatal("Must specify package name") 40 | } 41 | if *tag == "" { 42 | log.Fatal("Must specify Go build tag") 43 | } 44 | if *o == "" { 45 | log.Fatal("Must specify output file name") 46 | } 47 | 48 | tpls, err := tf.Get() 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | if tpls == nil { 53 | log.Fatalf("No template found") 54 | } 55 | 56 | var cmds []string 57 | for _, c := range tpls.Commands { 58 | cmds = append(cmds, c...) 59 | } 60 | for _, conf := range tpls.Configs { 61 | for _, c := range conf.Commands { 62 | cmds = append(cmds, tpls.CommandsFor(c.Commands...)...) 63 | } 64 | } 65 | paths, err := findpkg.ResolveGlobs(nil, golang.Default(), findpkg.DefaultEnv(), cmds) 66 | if err != nil { 67 | log.Fatal(err) 68 | } 69 | dedup := map[string]struct{}{} 70 | for _, p := range paths { 71 | dedup[p] = struct{}{} 72 | } 73 | c := maps.Keys(dedup) 74 | sort.Strings(c) 75 | 76 | tpl := `//go:build {{.Tag}} 77 | 78 | package {{.Package}} 79 | 80 | import ({{range .Imports}} 81 | _ "{{.}}"{{end}} 82 | ) 83 | ` 84 | 85 | vars := struct { 86 | Tag string 87 | Package string 88 | Imports []string 89 | }{ 90 | Tag: *tag, 91 | Package: *pkg, 92 | Imports: c, 93 | } 94 | t := template.Must(template.New("tpl").Parse(tpl)) 95 | var b bytes.Buffer 96 | if err := t.Execute(&b, vars); err != nil { 97 | log.Fatal(err) 98 | } 99 | if err := os.WriteFile(*o, b.Bytes(), 0o644); err != nil { 100 | log.Fatal(err) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /cmd/mkuimage/.gitignore: -------------------------------------------------------------------------------- 1 | mkuimage 2 | cover 3 | -------------------------------------------------------------------------------- /cmd/mkuimage/main.go: -------------------------------------------------------------------------------- 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 | // Command mkuimage builds CPIO archives with the given files and Go commands. 6 | package main 7 | 8 | import ( 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "log" 13 | "log/slog" 14 | "os" 15 | 16 | "github.com/dustin/go-humanize" 17 | "github.com/u-root/gobusybox/src/pkg/golang" 18 | "github.com/u-root/mkuimage/uimage" 19 | "github.com/u-root/mkuimage/uimage/mkuimage" 20 | "github.com/u-root/uio/llog" 21 | ) 22 | 23 | var ( 24 | errEmptyFilesArg = errors.New("empty argument to -files") 25 | ) 26 | 27 | // checkArgs checks for common mistakes that cause confusion. 28 | // 1. -files as the last argument 29 | // 2. -files followed by any switch, indicating a shell expansion problem 30 | // This is usually caused by Makfiles structured as follows 31 | // u-root -files `which ethtool` -files `which bash` 32 | // if ethtool is not installed, the expansion yields 33 | // u-root -files -files `which bash` 34 | // and the rather confusing error message 35 | // 16:14:51 Skipping /usr/bin/bash because it is not a directory 36 | // which, in practice, nobody understands 37 | func checkArgs(args ...string) error { 38 | if len(args) == 0 { 39 | return nil 40 | } 41 | 42 | if args[len(args)-1] == "-files" { 43 | return fmt.Errorf("last argument is -files:%w", errEmptyFilesArg) 44 | } 45 | 46 | // We know the last arg is not -files; scan the arguments for -files 47 | // followed by a switch. 48 | for i := 0; i < len(args)-1; i++ { 49 | if args[i] == "-files" && args[i+1][0] == '-' { 50 | return fmt.Errorf("-files argument %d is followed by a switch: %w", i, errEmptyFilesArg) 51 | } 52 | } 53 | 54 | return nil 55 | } 56 | 57 | func main() { 58 | log.SetFlags(log.Ltime) 59 | if err := checkArgs(os.Args...); err != nil { 60 | log.Fatal(err) 61 | } 62 | 63 | env := golang.Default(golang.DisableCGO()) 64 | f := &mkuimage.Flags{ 65 | Commands: mkuimage.CommandFlags{Builder: "bb"}, 66 | ArchiveFormat: "cpio", 67 | OutputFile: defaultFile(env), 68 | } 69 | f.RegisterFlags(flag.CommandLine) 70 | 71 | l := llog.Default() 72 | l.RegisterVerboseFlag(flag.CommandLine, "v", slog.LevelDebug) 73 | 74 | tf := &mkuimage.TemplateFlags{} 75 | tf.RegisterFlags(flag.CommandLine) 76 | flag.Parse() 77 | 78 | // Set defaults. 79 | m := []uimage.Modifier{ 80 | uimage.WithReplaceEnv(env), 81 | uimage.WithBaseArchive(uimage.DefaultRamfs()), 82 | uimage.WithCPIOOutput(defaultFile(env)), 83 | } 84 | if err := mkuimage.CreateUimage(l, m, tf, f, flag.Args()); err != nil { 85 | l.Errorf("mkuimage error: %v", err) 86 | os.Exit(1) 87 | } 88 | 89 | if stat, err := os.Stat(f.OutputFile); err == nil && f.ArchiveFormat == "cpio" { 90 | l.Infof("Successfully built %q (size %d bytes -- %s).", f.OutputFile, stat.Size(), humanize.IBytes(uint64(stat.Size()))) 91 | } 92 | } 93 | 94 | func defaultFile(env *golang.Environ) string { 95 | if len(env.GOOS) == 0 || len(env.GOARCH) == 0 { 96 | return "/tmp/initramfs.cpio" 97 | } 98 | return fmt.Sprintf("/tmp/initramfs.%s_%s.cpio", env.GOOS, env.GOARCH) 99 | } 100 | -------------------------------------------------------------------------------- /cmd/mkuimage/main_test.go: -------------------------------------------------------------------------------- 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 | "bytes" 9 | "crypto/sha256" 10 | "errors" 11 | "fmt" 12 | "io" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "regexp" 17 | "strings" 18 | "syscall" 19 | "testing" 20 | 21 | "github.com/u-root/gobusybox/src/pkg/golang" 22 | "github.com/u-root/mkuimage/cpio" 23 | itest "github.com/u-root/mkuimage/uimage/initramfs/test" 24 | "golang.org/x/sync/errgroup" 25 | ) 26 | 27 | func hasTempDir(t *testing.T, output string) { 28 | t.Helper() 29 | tempDir := regexp.MustCompile(`Keeping temp dir (.+)`).FindStringSubmatch(output) 30 | if tempDir == nil { 31 | t.Errorf("Keeping temp dir not found in output") 32 | return 33 | } 34 | if fi, err := os.Stat(tempDir[1]); err != nil { 35 | t.Error(err) 36 | } else if !fi.IsDir() { 37 | t.Errorf("Stat(%s) = %v, want directory", tempDir[1], fi) 38 | } 39 | } 40 | 41 | func dirExists(name string) func(t *testing.T, output string) { 42 | return func(t *testing.T, output string) { 43 | t.Helper() 44 | if fi, err := os.Stat(name); err != nil { 45 | t.Error(err) 46 | } else if !fi.IsDir() { 47 | t.Errorf("Stat(%s) = %v, want directory", name, fi) 48 | } 49 | } 50 | } 51 | 52 | func TestUrootCmdline(t *testing.T) { 53 | wd, err := os.Getwd() 54 | if err != nil { 55 | t.Fatal(err) 56 | } 57 | 58 | execPath := filepath.Join(t.TempDir(), "binary") 59 | // Build the stuff. 60 | // NoTrimPath ensures that the right Go version is used when running the tests. 61 | goEnv := golang.Default(golang.DisableCGO()) 62 | if err := goEnv.BuildDir(wd, execPath, &golang.BuildOpts{NoStrip: true, NoTrimPath: true, ExtraArgs: []string{"-cover"}}); err != nil { 63 | t.Fatal(err) 64 | } 65 | 66 | gocoverdir := filepath.Join(wd, "cover") 67 | os.RemoveAll(gocoverdir) 68 | if err := os.Mkdir(gocoverdir, 0o777); err != nil && !os.IsExist(err) { 69 | t.Fatal(err) 70 | } 71 | 72 | samplef, err := os.CreateTemp(t.TempDir(), "u-root-test-") 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | samplef.Close() 77 | 78 | sampledir := t.TempDir() 79 | if err = os.WriteFile(filepath.Join(sampledir, "foo"), nil, 0o644); err != nil { 80 | t.Fatal(err) 81 | } 82 | if err = os.WriteFile(filepath.Join(sampledir, "bar"), nil, 0o644); err != nil { 83 | t.Fatal(err) 84 | } 85 | tempDir := filepath.Join(t.TempDir(), "tempdir") 86 | 87 | type testCase struct { 88 | name string 89 | env []string 90 | args []string 91 | exitCode int 92 | validators []itest.ArchiveValidator 93 | wantOutput func(*testing.T, string) 94 | } 95 | 96 | noCmdTests := []testCase{ 97 | { 98 | name: "include one extra file", 99 | args: []string{"-nocmd", "-files=/bin/bash"}, 100 | env: []string{"GO111MODULE=off"}, 101 | validators: []itest.ArchiveValidator{ 102 | itest.HasFile{Path: "bin/bash"}, 103 | }, 104 | }, 105 | { 106 | name: "fix usage of an absolute path", 107 | args: []string{"-nocmd", fmt.Sprintf("-files=%s:/bin", sampledir)}, 108 | env: []string{"GO111MODULE=off"}, 109 | validators: []itest.ArchiveValidator{ 110 | itest.HasFile{Path: "/bin/foo"}, 111 | itest.HasFile{Path: "/bin/bar"}, 112 | }, 113 | }, 114 | { 115 | name: "include multiple extra files", 116 | args: []string{"-nocmd", "-files=/bin/bash", "-files=/bin/ls", fmt.Sprintf("-files=%s", samplef.Name())}, 117 | env: []string{"GO111MODULE=off"}, 118 | validators: []itest.ArchiveValidator{ 119 | itest.HasFile{Path: "bin/bash"}, 120 | itest.HasFile{Path: "bin/ls"}, 121 | itest.HasFile{Path: samplef.Name()}, 122 | }, 123 | }, 124 | { 125 | name: "include one extra file with rename", 126 | args: []string{"-nocmd", "-files=/bin/bash:bin/bush"}, 127 | env: []string{"GO111MODULE=off"}, 128 | validators: []itest.ArchiveValidator{ 129 | itest.HasFile{Path: "bin/bush"}, 130 | }, 131 | }, 132 | { 133 | name: "supplied file can be uinit", 134 | args: []string{"-nocmd", "-files=/bin/bash:bin/bash", "-uinitcmd=/bin/bash"}, 135 | env: []string{"GO111MODULE=off"}, 136 | validators: []itest.ArchiveValidator{ 137 | itest.HasFile{Path: "bin/bash"}, 138 | itest.HasRecord{R: cpio.Symlink("bin/uinit", "bash")}, 139 | }, 140 | }, 141 | } 142 | 143 | bareTests := []testCase{ 144 | { 145 | name: "uinitcmd", 146 | args: []string{"-uinitcmd=echo foobar fuzz", "-defaultsh=", "github.com/u-root/u-root/cmds/core/init", "github.com/u-root/u-root/cmds/core/echo"}, 147 | validators: []itest.ArchiveValidator{ 148 | itest.HasRecord{R: cpio.Symlink("bin/uinit", "../bbin/echo")}, 149 | itest.HasContent{ 150 | Path: "etc/uinit.flags", 151 | Content: "\"foobar\"\n\"fuzz\"", 152 | }, 153 | }, 154 | }, 155 | { 156 | name: "binary build", 157 | args: []string{ 158 | "-build=binary", 159 | "-defaultsh=", 160 | "github.com/u-root/u-root/cmds/core/init", 161 | "github.com/u-root/u-root/cmds/core/echo", 162 | }, 163 | validators: []itest.ArchiveValidator{ 164 | itest.HasFile{Path: "bin/init"}, 165 | itest.HasFile{Path: "bin/echo"}, 166 | itest.HasRecord{R: cpio.CharDev("dev/tty", 0o666, 5, 0)}, 167 | }, 168 | }, 169 | { 170 | name: "hosted mode", 171 | args: []string{ 172 | "-base=/dev/null", 173 | "-defaultsh=", 174 | "-initcmd=", 175 | "github.com/u-root/u-root/cmds/core/ls", 176 | "github.com/u-root/u-root/cmds/core/init", 177 | }, 178 | }, 179 | { 180 | name: "AMD64 build", 181 | env: []string{"GOARCH=amd64"}, 182 | args: []string{ 183 | "-defaultsh=echo", 184 | "github.com/u-root/u-root/cmds/core/echo", 185 | "github.com/u-root/u-root/cmds/core/init", 186 | }, 187 | }, 188 | { 189 | name: "AMD64 build with temp dir", 190 | env: []string{"GOARCH=amd64"}, 191 | args: []string{ 192 | "--keep-tmp-dir", 193 | "--defaultsh=echo", 194 | "github.com/u-root/u-root/cmds/core/echo", 195 | "github.com/u-root/u-root/cmds/core/init", 196 | }, 197 | exitCode: 1, 198 | wantOutput: hasTempDir, 199 | }, 200 | { 201 | name: "ARM7 build", 202 | env: []string{"GOARCH=arm", "GOARM=7"}, 203 | args: []string{ 204 | "-defaultsh=", 205 | "github.com/u-root/u-root/cmds/core/init", 206 | "github.com/u-root/u-root/cmds/core/echo", 207 | }, 208 | }, 209 | { 210 | name: "ARM64 build", 211 | env: []string{"GOARCH=arm64"}, 212 | args: []string{ 213 | "-defaultsh=", 214 | "github.com/u-root/u-root/cmds/core/init", 215 | "github.com/u-root/u-root/cmds/core/echo", 216 | }, 217 | }, 218 | { 219 | name: "RISCV 64bit build", 220 | env: []string{"GOARCH=riscv64"}, 221 | args: []string{ 222 | "-defaultsh=", 223 | "github.com/u-root/u-root/cmds/core/init", 224 | "github.com/u-root/u-root/cmds/core/echo", 225 | }, 226 | }, 227 | { 228 | name: "build invalid", 229 | args: []string{ 230 | "-build=source", 231 | "github.com/u-root/u-root/cmds/core/init", 232 | "github.com/u-root/u-root/cmds/core/echo", 233 | }, 234 | exitCode: 1, 235 | }, 236 | { 237 | name: "arch invalid preserves temp dir", 238 | env: []string{"GOARCH=doesnotexist"}, 239 | args: []string{ 240 | "--defaultsh=echo", 241 | "github.com/u-root/u-root/cmds/core/echo", 242 | "github.com/u-root/u-root/cmds/core/init", 243 | }, 244 | exitCode: 1, 245 | wantOutput: hasTempDir, 246 | }, 247 | { 248 | name: "specify temp dir", 249 | args: []string{ 250 | "--tmp-dir=" + tempDir, 251 | "github.com/u-root/u-root/cmds/core/echo", 252 | "github.com/u-root/u-root/cmds/core/init", 253 | }, 254 | exitCode: 1, 255 | wantOutput: dirExists(tempDir), 256 | }, 257 | { 258 | name: "template config", 259 | args: []string{"-config-file=./testdata/test-config.yaml", "-v", "-config=coreconf"}, 260 | validators: []itest.ArchiveValidator{ 261 | itest.HasRecord{R: cpio.CharDev("dev/tty", 0o666, 5, 0)}, 262 | itest.HasFile{Path: "bbin/bb"}, 263 | itest.HasRecord{R: cpio.Symlink("bbin/echo", "bb")}, 264 | itest.HasRecord{R: cpio.Symlink("bbin/ip", "bb")}, 265 | itest.HasRecord{R: cpio.Symlink("bbin/init", "bb")}, 266 | itest.HasRecord{R: cpio.Symlink("init", "bbin/init")}, 267 | itest.HasRecord{R: cpio.Symlink("bin/sh", "../bbin/echo")}, 268 | itest.HasRecord{R: cpio.Symlink("bin/uinit", "../bbin/echo")}, 269 | itest.HasRecord{R: cpio.Symlink("bin/defaultsh", "../bbin/echo")}, 270 | itest.HasContent{ 271 | Path: "etc/uinit.flags", 272 | Content: "\"script.sh\"", 273 | }, 274 | }, 275 | }, 276 | { 277 | name: "template command", 278 | args: []string{"-config-file=./testdata/test-config.yaml", "-v", "core"}, 279 | validators: []itest.ArchiveValidator{ 280 | itest.HasRecord{R: cpio.CharDev("dev/tty", 0o666, 5, 0)}, 281 | itest.HasFile{Path: "bbin/bb"}, 282 | itest.HasRecord{R: cpio.Symlink("bbin/echo", "bb")}, 283 | itest.HasRecord{R: cpio.Symlink("bbin/ip", "bb")}, 284 | itest.HasRecord{R: cpio.Symlink("bbin/init", "bb")}, 285 | }, 286 | }, 287 | { 288 | name: "template config not found", 289 | args: []string{"-config-file=./testdata/test-config.yaml", "-v", "-config=foobar"}, 290 | exitCode: 1, 291 | }, 292 | { 293 | name: "builder not found", 294 | args: []string{"-v", "build=source"}, 295 | exitCode: 1, 296 | }, 297 | { 298 | name: "template file not found", 299 | args: []string{"-v", "-config-file=./testdata/doesnotexist"}, 300 | exitCode: 1, 301 | }, 302 | { 303 | name: "config not found with no default template", 304 | args: []string{"-v", "-config=foo"}, 305 | exitCode: 1, 306 | }, 307 | } 308 | 309 | for _, tt := range append(noCmdTests, bareTests...) { 310 | tt := tt 311 | t.Run(tt.name, func(t *testing.T) { 312 | var g errgroup.Group 313 | var f1, f2 *os.File 314 | var out string 315 | var sum1, sum2 []byte 316 | 317 | g.Go(func() error { 318 | var err error 319 | f1, out, sum1, err = buildIt(t, execPath, tt.args, tt.env, gocoverdir) 320 | return err 321 | }) 322 | 323 | g.Go(func() error { 324 | var err error 325 | f2, _, sum2, err = buildIt(t, execPath, tt.args, tt.env, gocoverdir) 326 | return err 327 | }) 328 | 329 | err := g.Wait() 330 | if tt.wantOutput != nil { 331 | tt.wantOutput(t, out) 332 | } 333 | 334 | var exitErr *exec.ExitError 335 | if errors.As(err, &exitErr) { 336 | if ec := exitErr.Sys().(syscall.WaitStatus).ExitStatus(); ec != tt.exitCode { 337 | t.Errorf("mkuimage exit code = %d, want %d", ec, tt.exitCode) 338 | } 339 | return 340 | } else if err != nil { 341 | t.Errorf("mkuimage failed: %v", err) 342 | return 343 | } 344 | 345 | a, err := itest.ReadArchive(f1.Name()) 346 | if err != nil { 347 | t.Fatal(err) 348 | } 349 | for _, v := range tt.validators { 350 | if err := v.Validate(a); err != nil { 351 | t.Errorf("validator failed: %v / archive:\n%s", err, a) 352 | } 353 | } 354 | 355 | if !bytes.Equal(sum1, sum2) { 356 | t.Errorf("not reproducible, hashes don't match") 357 | t.Errorf("env: %v args: %v", tt.env, tt.args) 358 | t.Errorf("file1: %v file2: %v", f1.Name(), f2.Name()) 359 | } 360 | }) 361 | } 362 | } 363 | 364 | func buildIt(t *testing.T, execPath string, args, env []string, gocoverdir string) (*os.File, string, []byte, error) { 365 | t.Helper() 366 | initramfs, err := os.CreateTemp(t.TempDir(), "u-root-") 367 | if err != nil { 368 | return nil, "", nil, err 369 | } 370 | 371 | // Use the u-root command outside of the $GOPATH tree to make sure it 372 | // still works. 373 | args = append([]string{"-o", initramfs.Name()}, args...) 374 | t.Logf("Commandline: %v mkuimage %v", strings.Join(env, " "), strings.Join(args, " ")) 375 | 376 | c := exec.Command(execPath, args...) 377 | c.Env = append(os.Environ(), env...) 378 | c.Env = append(c.Env, "GOCOVERDIR="+gocoverdir) 379 | out, err := c.CombinedOutput() 380 | t.Logf("Output:\n%s", out) 381 | if err != nil { 382 | return nil, string(out), nil, err 383 | } 384 | 385 | h1 := sha256.New() 386 | if _, err := io.Copy(h1, initramfs); err != nil { 387 | return nil, string(out), nil, err 388 | } 389 | return initramfs, string(out), h1.Sum(nil), nil 390 | } 391 | 392 | func TestCheckArgs(t *testing.T) { 393 | for _, tt := range []struct { 394 | name string 395 | args []string 396 | err error 397 | }{ 398 | {"-files is only arg", []string{"-files"}, errEmptyFilesArg}, 399 | {"-files followed by -files", []string{"-files", "-files"}, errEmptyFilesArg}, 400 | {"-files followed by any other switch", []string{"-files", "-abc"}, errEmptyFilesArg}, 401 | {"no args", []string{}, nil}, 402 | {"u-root alone", []string{"u-root"}, nil}, 403 | {"u-root with -files and other args", []string{"u-root", "-files", "/bin/bash", "core"}, nil}, 404 | } { 405 | t.Run(tt.name, func(t *testing.T) { 406 | if err := checkArgs(tt.args...); !errors.Is(err, tt.err) { 407 | t.Errorf("%q: got %v, want %v", tt.args, err, tt.err) 408 | } 409 | }) 410 | } 411 | } 412 | -------------------------------------------------------------------------------- /cmd/mkuimage/testdata/test-config.yaml: -------------------------------------------------------------------------------- 1 | commands: 2 | core: 3 | - github.com/u-root/u-root/cmds/core/ip 4 | - github.com/u-root/u-root/cmds/core/init 5 | - github.com/u-root/u-root/cmds/core/echo 6 | 7 | minimal: 8 | - github.com/u-root/u-root/cmds/core/ls 9 | - github.com/u-root/u-root/cmds/core/init 10 | 11 | plan9: 12 | - github.com/u-root/u-root/cmds/core/ls 13 | - github.com/u-root/u-root/cmds/core/init 14 | - github.com/u-root/u-root/cmds/core/cat 15 | 16 | configs: 17 | plan9: 18 | goarch: amd64 19 | goos: plan9 20 | build_tags: [grpcnotrace] 21 | files: 22 | - /bin/bash 23 | init: init 24 | uinit: cat script.sh 25 | shell: cat 26 | commands: 27 | - builder: bb 28 | commands: [plan9] 29 | 30 | coreconf: 31 | build_tags: [grpcnotrace] 32 | init: init 33 | uinit: echo script.sh 34 | shell: echo 35 | commands: 36 | - builder: bb 37 | commands: [core, minimal] 38 | -------------------------------------------------------------------------------- /cpio/archive.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import ( 8 | "io" 9 | "strings" 10 | ) 11 | 12 | // Archive is an in-memory list of files. 13 | // 14 | // Archive itself is a RecordWriter, and Archive.Reader() returns a new 15 | // RecordReader for the archive starting from the first file. 16 | type Archive struct { 17 | // Files is a map of relative archive path -> record. 18 | Files map[string]Record 19 | 20 | // Order is a list of relative archive paths and represents the order 21 | // in which Files were added. 22 | Order []string 23 | } 24 | 25 | // InMemArchive returns an in-memory file archive. 26 | func InMemArchive() *Archive { 27 | return &Archive{ 28 | Files: make(map[string]Record), 29 | } 30 | } 31 | 32 | // ArchiveFromRecords creates a new Archive from the records. 33 | func ArchiveFromRecords(rs []Record) (*Archive, error) { 34 | a := InMemArchive() 35 | for _, r := range rs { 36 | if err := a.WriteRecord(r); err != nil { 37 | return nil, err 38 | } 39 | } 40 | return a, nil 41 | } 42 | 43 | // ArchiveFromReader reads records from r into a new Archive in memory. 44 | func ArchiveFromReader(r RecordReader) (*Archive, error) { 45 | a := InMemArchive() 46 | if err := Copy(a, r, nil); err != nil { 47 | return nil, err 48 | } 49 | return a, nil 50 | } 51 | 52 | // WriteRecord implements RecordWriter and adds a record to the archive. 53 | // 54 | // WriteRecord uses Normalize to deduplicate paths. 55 | func (a *Archive) WriteRecord(r Record) error { 56 | r.Name = Normalize(r.Name) 57 | a.Files[r.Name] = r 58 | a.Order = append(a.Order, r.Name) 59 | return nil 60 | } 61 | 62 | // Empty returns whether the archive has any files in it. 63 | func (a *Archive) Empty() bool { 64 | return len(a.Files) == 0 65 | } 66 | 67 | // Contains returns true if a record matching r is in the archive. 68 | func (a *Archive) Contains(r Record) bool { 69 | r.Name = Normalize(r.Name) 70 | if s, ok := a.Files[r.Name]; ok { 71 | return Equal(r, s) 72 | } 73 | return false 74 | } 75 | 76 | // Get returns a record for the normalized path or false if there is none. 77 | // 78 | // The path is normalized using Normalize, so Get("/bin/bar") is the same as 79 | // Get("bin/bar") is the same as Get("bin//bar"). 80 | func (a *Archive) Get(path string) (Record, bool) { 81 | r, ok := a.Files[Normalize(path)] 82 | return r, ok 83 | } 84 | 85 | // String implements fmt.Stringer. 86 | // 87 | // String lists files like ls would. 88 | func (a *Archive) String() string { 89 | var b strings.Builder 90 | r := a.Reader() 91 | for { 92 | record, err := r.ReadRecord() 93 | if err != nil { 94 | return b.String() 95 | } 96 | b.WriteString(record.String()) 97 | b.WriteString("\n") 98 | } 99 | } 100 | 101 | type archiveReader struct { 102 | a *Archive 103 | pos int 104 | } 105 | 106 | // Reader returns a RecordReader for the archive that starts at the first 107 | // record. 108 | func (a *Archive) Reader() RecordReader { 109 | return &EOFReader{&archiveReader{a: a}} 110 | } 111 | 112 | // ReadRecord implements RecordReader. 113 | func (ar *archiveReader) ReadRecord() (Record, error) { 114 | if ar.pos >= len(ar.a.Order) { 115 | return Record{}, io.EOF 116 | } 117 | 118 | path := ar.a.Order[ar.pos] 119 | ar.pos++ 120 | return ar.a.Files[path], nil 121 | } 122 | -------------------------------------------------------------------------------- /cpio/archive_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2022 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 cpio 6 | 7 | import ( 8 | "fmt" 9 | "syscall" 10 | "testing" 11 | ) 12 | 13 | func FuzzWriteReadInMemArchive(f *testing.F) { 14 | var fileCount uint64 = 4 15 | var content = []byte("Content") 16 | var name = "fileName" 17 | var ino, mode, uid, gid, nlink, mtime, major, minor, rmajor, rminor uint64 = 1, S_IFREG | 2, 3, 4, 5, 6, 7, 8, 9, 10 18 | f.Add(fileCount, content, name, ino, mode, uid, gid, nlink, mtime, major, minor, rmajor, rminor) 19 | f.Fuzz(func(t *testing.T, fileCount uint64, content []byte, name string, ino uint64, mode uint64, uid uint64, gid uint64, nlink uint64, mtime uint64, major uint64, minor uint64, rmajor uint64, rminor uint64) { 20 | if len(name) > 64 || len(content) > 200 || fileCount > 8 { 21 | return 22 | } 23 | recs := []Record{} 24 | var i uint64 25 | for i = 0; i < fileCount; i++ { 26 | recs = append(recs, StaticRecord(content, Info{ 27 | Ino: ino | i, 28 | Mode: syscall.S_IFREG | mode | i, 29 | UID: uid | i, 30 | GID: gid | i, 31 | NLink: nlink | i, 32 | MTime: mtime | i, 33 | FileSize: uint64(len(content)), 34 | Major: major | i, 35 | Minor: minor | i, 36 | Rmajor: rmajor | i, 37 | Rminor: rminor | i, 38 | Name: Normalize(name) + fmt.Sprintf("%d", i), 39 | })) 40 | } 41 | 42 | arch, err := ArchiveFromRecords(recs) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | 47 | archReader := arch.Reader() 48 | for _, rec := range recs { 49 | readRec, err := archReader.ReadRecord() 50 | if err != nil { 51 | t.Fatalf("failed to read record from archive") 52 | } 53 | 54 | if !Equal(rec, readRec) { 55 | t.Fatalf("records not equal: %v %v", rec, readRec) 56 | } 57 | if !arch.Contains(rec) { 58 | t.Fatalf("record not in archive %v %#v", rec, arch) 59 | } 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /cpio/const.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | // These Unix constants are needed everywhere cpio is used, Unix or not. 8 | // But we are unable to import the unix package when plan 9 is enabled, 9 | // so lucky us, the numbers have been the same for half a century. 10 | // It is ok to just define them. 11 | // nolint 12 | const ( 13 | S_IEXEC = 0x40 14 | S_IFBLK = 0x6000 15 | S_IFCHR = 0x2000 16 | S_IFDIR = 0x4000 17 | S_IFIFO = 0x1000 18 | S_IFLNK = 0xa000 19 | S_IFMT = 0xf000 20 | S_IFREG = 0x8000 21 | S_IFSOCK = 0xc000 22 | S_IFWHT = 0xe000 23 | S_IREAD = 0x100 24 | S_IRGRP = 0x20 25 | S_IROTH = 0x4 26 | S_IRUSR = 0x100 27 | S_IRWXG = 0x38 28 | S_IRWXO = 0x7 29 | S_IRWXU = 0x1c0 30 | S_ISGID = 0x400 31 | S_ISTXT = 0x200 32 | S_ISUID = 0x800 33 | S_ISVTX = 0x200 34 | ) 35 | 36 | // Unix mode_t bits. 37 | const ( 38 | modeTypeMask = 0o170000 39 | modeSocket = 0o140000 40 | modeSymlink = 0o120000 41 | modeFile = 0o100000 42 | modeBlock = 0o060000 43 | modeDir = 0o040000 44 | modeChar = 0o020000 45 | modeFIFO = 0o010000 46 | modeSUID = 0o004000 47 | modeSGID = 0o002000 48 | modeSticky = 0o001000 49 | modePermissions = 0o000777 50 | ) 51 | -------------------------------------------------------------------------------- /cpio/cpio.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio implements utilities for reading and writing cpio archives. 6 | // 7 | // Currently, only newc-formatted cpio archives are supported through cpio.Newc. 8 | // 9 | // Reading from or writing to a file: 10 | // 11 | // f, err := os.Open(...) 12 | // if err ... 13 | // recReader := cpio.Newc.Reader(f) 14 | // err := ForEachRecord(recReader, func(r cpio.Record) error { 15 | // 16 | // }) 17 | // 18 | // // Or... 19 | // recWriter := cpio.Newc.Writer(f) 20 | // 21 | // Reading from or writing to an in-memory archive: 22 | // 23 | // a := cpio.InMemArchive() 24 | // err := a.WriteRecord(...) 25 | // 26 | // recReader := a.Reader() // Reads from the "beginning." 27 | // 28 | // if a.Contains("bar/foo") { 29 | // 30 | // } 31 | package cpio 32 | 33 | import ( 34 | "fmt" 35 | "io" 36 | "os" 37 | "time" 38 | ) 39 | 40 | var ( 41 | formatMap = make(map[string]RecordFormat) 42 | 43 | // Debug can be set e.g. to log.Printf to enable debug prints from 44 | // marshaling/unmarshaling cpio archives. 45 | Debug = func(string, ...interface{}) {} 46 | ) 47 | 48 | // Record represents a CPIO record, which represents a Unix file. 49 | type Record struct { 50 | // ReaderAt contains the content of this CPIO record. 51 | io.ReaderAt 52 | 53 | // Info is metadata describing the CPIO record. 54 | Info 55 | 56 | // metadata about this item's place in the file 57 | RecPos int64 // Where in the file this record is 58 | RecLen uint64 // How big the record is. 59 | FilePos int64 // Where in the CPIO the file's contents are. 60 | } 61 | 62 | // Info holds metadata about files. 63 | type Info struct { 64 | Ino uint64 65 | Mode uint64 66 | UID uint64 67 | GID uint64 68 | NLink uint64 69 | MTime uint64 70 | FileSize uint64 71 | Dev uint64 72 | Major uint64 73 | Minor uint64 74 | Rmajor uint64 75 | Rminor uint64 76 | Name string 77 | } 78 | 79 | func (i Info) String() string { 80 | return fmt.Sprintf("%s: Ino %d Mode %#o UID %d GID %d NLink %d MTime %v FileSize %d Major %d Minor %d Rmajor %d Rminor %d", 81 | i.Name, 82 | i.Ino, 83 | i.Mode, 84 | i.UID, 85 | i.GID, 86 | i.NLink, 87 | time.Unix(int64(i.MTime), 0).UTC(), 88 | i.FileSize, 89 | i.Major, 90 | i.Minor, 91 | i.Rmajor, 92 | i.Rminor) 93 | } 94 | 95 | // A RecordReader reads one record from an archive. 96 | type RecordReader interface { 97 | ReadRecord() (Record, error) 98 | } 99 | 100 | // A RecordWriter writes one record to an archive. 101 | type RecordWriter interface { 102 | WriteRecord(Record) error 103 | } 104 | 105 | // A RecordFormat gives readers and writers for dealing with archives from io 106 | // objects. 107 | // 108 | // CPIO files have a number of records, of which newc is the most widely used 109 | // today. 110 | type RecordFormat interface { 111 | Reader(r io.ReaderAt) RecordReader 112 | FileReader(f *os.File) RecordReader 113 | Writer(w io.Writer) RecordWriter 114 | } 115 | 116 | // Format returns the RecordFormat with that name, if it exists. 117 | func Format(name string) (RecordFormat, error) { 118 | op, ok := formatMap[name] 119 | if !ok { 120 | return nil, fmt.Errorf("%q is not in cpio format map %v", name, formatMap) 121 | } 122 | return op, nil 123 | } 124 | -------------------------------------------------------------------------------- /cpio/fs_plan9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "syscall" 14 | 15 | "github.com/u-root/mkuimage/cpio/internal/upath" 16 | "github.com/u-root/uio/uio" 17 | ) 18 | 19 | // A Recorder is a structure that contains variables used to calculate 20 | // file parameters such as inode numbers for a CPIO file. The life-time 21 | // of a Record structure is meant to be the same as the construction of a 22 | // single CPIO archive. Do not reuse between CPIOs if you don't know what 23 | // you're doing. 24 | type Recorder struct { 25 | inumber uint64 26 | } 27 | 28 | var modeMap = map[uint64]os.FileMode{ 29 | modeFile: 0, 30 | modeDir: os.ModeDir, 31 | } 32 | 33 | func unixModeToFileType(m uint64) (os.FileMode, error) { 34 | if t, ok := modeMap[m&modeTypeMask]; ok { 35 | return t, nil 36 | } 37 | return 0, fmt.Errorf("invalid file type %#o", m&modeTypeMask) 38 | } 39 | 40 | func toFileMode(r Record) os.FileMode { 41 | return os.FileMode(perm(r)) 42 | } 43 | 44 | // setModes sets the modes. 45 | func setModes(r Record) error { 46 | if err := os.Chmod(r.Name, toFileMode(r)&os.ModePerm); err != nil { 47 | return err 48 | } 49 | return nil 50 | } 51 | 52 | func perm(r Record) uint32 { 53 | return uint32(r.Mode) & modePermissions 54 | } 55 | 56 | func dev(r Record) int { 57 | return int(r.Rmajor<<8 | r.Rminor) 58 | } 59 | 60 | // CreateFile creates a local file for f relative to the current working 61 | // directory. 62 | // 63 | // CreateFile will attempt to set all metadata for the file, including 64 | // ownership, times, and permissions. 65 | func CreateFile(f Record) error { 66 | return CreateFileInRoot(f, ".", true) 67 | } 68 | 69 | // CreateFileInRoot creates a local file for f relative to rootDir. 70 | func CreateFileInRoot(f Record, rootDir string, forcePriv bool) error { 71 | m, err := unixModeToFileType(f.Mode) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | f.Name, err = upath.SafeFilepathJoin(rootDir, f.Name) 77 | if err != nil { 78 | // The behavior is to skip files which are unsafe due to 79 | // zipslip, but continue extracting everything else. 80 | log.Printf("Warning: Skipping file %q due to: %v", f.Name, err) 81 | return nil 82 | } 83 | dir := filepath.Dir(f.Name) 84 | // The problem: many cpio archives do not specify the directories and 85 | // hence the permissions. They just specify the whole path. In order 86 | // to create files in these directories, we have to make them at least 87 | // mode 755. 88 | if _, err := os.Stat(dir); os.IsNotExist(err) && len(dir) > 0 { 89 | if err := os.MkdirAll(dir, 0o755); err != nil { 90 | return fmt.Errorf("CreateFileInRoot %q: %v", f.Name, err) 91 | } 92 | } 93 | 94 | switch m { 95 | case os.FileMode(0): 96 | nf, err := os.Create(f.Name) 97 | if err != nil { 98 | return err 99 | } 100 | defer nf.Close() 101 | if _, err := io.Copy(nf, uio.Reader(f)); err != nil { 102 | return err 103 | } 104 | 105 | case os.ModeDir: 106 | if err := os.MkdirAll(f.Name, toFileMode(f)); err != nil { 107 | return err 108 | } 109 | 110 | default: 111 | return fmt.Errorf("%v: Unknown type %#o", f.Name, m) 112 | } 113 | 114 | if err := setModes(f); err != nil && forcePriv { 115 | return err 116 | } 117 | return nil 118 | } 119 | 120 | func (r *Recorder) inode(i Info) Info { 121 | i.Ino = r.inumber 122 | r.inumber++ 123 | return i 124 | } 125 | 126 | // GetRecord returns a cpio Record for the given path on the local file system. 127 | // 128 | // GetRecord does not follow symlinks. If path is a symlink, the record 129 | // returned will reflect that symlink. 130 | func (r *Recorder) GetRecord(path string) (Record, error) { 131 | fi, err := os.Stat(path) 132 | if err != nil { 133 | return Record{}, err 134 | } 135 | 136 | sys := fi.Sys().(*syscall.Dir) 137 | info := r.inode(sysInfo(path, sys)) 138 | 139 | switch fi.Mode() & os.ModeType { 140 | case 0: // Regular file. 141 | return Record{Info: info, ReaderAt: uio.NewLazyFile(path)}, nil 142 | default: 143 | return StaticRecord(nil, info), nil 144 | } 145 | } 146 | 147 | // NewRecorder creates a new Recorder. 148 | // 149 | // A recorder is a structure that contains variables used to calculate 150 | // file parameters such as inode numbers for a CPIO file. The life-time 151 | // of a Record structure is meant to be the same as the construction of a 152 | // single CPIO archive. Do not reuse between CPIOs if you don't know what 153 | // you're doing. 154 | func NewRecorder() *Recorder { 155 | return &Recorder{inumber: 2} 156 | } 157 | -------------------------------------------------------------------------------- /cpio/fs_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 | //go:build !plan9 && !windows 6 | // +build !plan9,!windows 7 | 8 | package cpio 9 | 10 | import ( 11 | "fmt" 12 | "io" 13 | "log" 14 | "os" 15 | "path/filepath" 16 | "syscall" 17 | 18 | "github.com/u-root/mkuimage/cpio/internal/upath" 19 | "github.com/u-root/uio/uio" 20 | "golang.org/x/sys/unix" 21 | ) 22 | 23 | var modeMap = map[uint64]os.FileMode{ 24 | modeSocket: os.ModeSocket, 25 | modeSymlink: os.ModeSymlink, 26 | modeFile: 0, 27 | modeBlock: os.ModeDevice, 28 | modeDir: os.ModeDir, 29 | modeChar: os.ModeCharDevice, 30 | modeFIFO: os.ModeNamedPipe, 31 | } 32 | 33 | // setModes sets the modes, changing the easy ones first and the harder ones last. 34 | // In this way, we set as much as we can before bailing out. 35 | // N.B.: if you set something with S_ISUID, then change the owner, 36 | // the kernel (Linux, OSX, etc.) clears S_ISUID (a good idea). So, the simple thing: 37 | // Do the chmod operations in order of difficulty, and give up as soon as we fail. 38 | // Set the basic permissions -- not including SUID, GUID, etc. 39 | // Set the times 40 | // Set the owner 41 | // Set ALL the mode bits, in case we need to do SUID, etc. If we could not 42 | // set the owner, we won't even try this operation of course, so we won't 43 | // have SUID incorrectly set for the wrong user. 44 | func setModes(r Record) error { 45 | if err := os.Chmod(r.Name, toFileMode(r)&os.ModePerm); err != nil { 46 | return err 47 | } 48 | /*if err := os.Chtimes(r.Name, time.Time{}, time.Unix(int64(r.MTime), 0)); err != nil { 49 | return err 50 | }*/ 51 | if err := os.Chown(r.Name, int(r.UID), int(r.GID)); err != nil { 52 | return err 53 | } 54 | return os.Chmod(r.Name, toFileMode(r)) 55 | } 56 | 57 | func toFileMode(r Record) os.FileMode { 58 | m := os.FileMode(perm(r)) 59 | if r.Mode&unix.S_ISUID != 0 { 60 | m |= os.ModeSetuid 61 | } 62 | if r.Mode&unix.S_ISGID != 0 { 63 | m |= os.ModeSetgid 64 | } 65 | if r.Mode&unix.S_ISVTX != 0 { 66 | m |= os.ModeSticky 67 | } 68 | return m 69 | } 70 | 71 | func perm(r Record) uint32 { 72 | return uint32(r.Mode) & modePermissions 73 | } 74 | 75 | func dev(r Record) int { 76 | return int(r.Rmajor<<8 | r.Rminor) 77 | } 78 | 79 | func linuxModeToFileType(m uint64) (os.FileMode, error) { 80 | if t, ok := modeMap[m&modeTypeMask]; ok { 81 | return t, nil 82 | } 83 | return 0, fmt.Errorf("invalid file type %#o", m&modeTypeMask) 84 | } 85 | 86 | // CreateFile creates a local file for f relative to the current working 87 | // directory. 88 | // 89 | // CreateFile will attempt to set all metadata for the file, including 90 | // ownership, times, and permissions. 91 | func CreateFile(f Record) error { 92 | return CreateFileInRoot(f, ".", true) 93 | } 94 | 95 | // CreateFileInRoot creates a local file for f relative to rootDir. 96 | // 97 | // It will attempt to set all metadata for the file, including ownership, 98 | // times, and permissions. If these fail, it only returns an error if 99 | // forcePriv is true. 100 | // 101 | // Block and char device creation will only return error if forcePriv is true. 102 | func CreateFileInRoot(f Record, rootDir string, forcePriv bool) error { 103 | m, err := linuxModeToFileType(f.Mode) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | f.Name, err = upath.SafeFilepathJoin(rootDir, f.Name) 109 | if err != nil { 110 | // The behavior is to skip files which are unsafe due to 111 | // zipslip, but continue extracting everything else. 112 | log.Printf("Warning: Skipping file %q due to: %v", f.Name, err) 113 | return nil 114 | } 115 | dir := filepath.Dir(f.Name) 116 | // The problem: many cpio archives do not specify the directories and 117 | // hence the permissions. They just specify the whole path. In order 118 | // to create files in these directories, we have to make them at least 119 | // mode 755. 120 | if _, err := os.Stat(dir); os.IsNotExist(err) && len(dir) > 0 { 121 | if err := os.MkdirAll(dir, 0o755); err != nil { 122 | return fmt.Errorf("CreateFileInRoot %q: %v", f.Name, err) 123 | } 124 | } 125 | 126 | switch m { 127 | case os.ModeSocket, os.ModeNamedPipe: 128 | return fmt.Errorf("%q: type %v: cannot create IPC endpoints", f.Name, m) 129 | 130 | case os.ModeSymlink: 131 | content, err := io.ReadAll(uio.Reader(f)) 132 | if err != nil { 133 | return err 134 | } 135 | return os.Symlink(string(content), f.Name) 136 | 137 | case os.FileMode(0): 138 | nf, err := os.Create(f.Name) 139 | if err != nil { 140 | return err 141 | } 142 | defer nf.Close() 143 | if _, err := io.Copy(nf, uio.Reader(f)); err != nil { 144 | return err 145 | } 146 | 147 | case os.ModeDir: 148 | if err := os.MkdirAll(f.Name, toFileMode(f)); err != nil { 149 | return err 150 | } 151 | 152 | case os.ModeDevice: 153 | if err := mknod(f.Name, perm(f)|syscall.S_IFBLK, dev(f)); err != nil && forcePriv { 154 | return err 155 | } 156 | 157 | case os.ModeCharDevice: 158 | if err := mknod(f.Name, perm(f)|syscall.S_IFCHR, dev(f)); err != nil && forcePriv { 159 | return err 160 | } 161 | 162 | default: 163 | return fmt.Errorf("%v: Unknown type %#o", f.Name, m) 164 | } 165 | 166 | if err := setModes(f); err != nil && forcePriv { 167 | return err 168 | } 169 | return nil 170 | } 171 | 172 | // Inumber and devnumbers are unique to Unix-like 173 | // operating systems. You can not uniquely disambiguate a file in a 174 | // Unix system with just an inumber, you need a device number too. 175 | // To handle hard links (unique to Unix) we need to figure out if a 176 | // given file has been seen before. To do this we see if a file has the 177 | // same [dev,ino] tuple as one we have seen. If so, we won't bother 178 | // reading it in. 179 | 180 | type devInode struct { 181 | dev uint64 182 | ino uint64 183 | } 184 | 185 | // A Recorder is a structure that contains variables used to calculate 186 | // file parameters such as inode numbers for a CPIO file. The life-time 187 | // of a Record structure is meant to be the same as the construction of a 188 | // single CPIO archive. Do not reuse between CPIOs if you don't know what 189 | // you're doing. 190 | type Recorder struct { 191 | inodeMap map[devInode]Info 192 | inumber uint64 193 | } 194 | 195 | // Certain elements of the file can not be set by cpio: 196 | // the Inode # 197 | // the Dev 198 | // maintaining these elements leaves us with a non-reproducible 199 | // output stream. In this function, we figure out what inumber 200 | // we need to use, and clear out anything we can. 201 | // We always zero the Dev. 202 | // We try to find the matching inode. If found, we use its inumber. 203 | // If not, we get a new inumber for it and save the inode away. 204 | // This eliminates two of the messier parts of creating reproducible 205 | // output streams. 206 | // The second return value indicates whether it is a hardlink or not. 207 | func (r *Recorder) inode(i Info) (Info, bool) { 208 | d := devInode{dev: i.Dev, ino: i.Ino} 209 | i.Dev = 0 210 | 211 | if d, ok := r.inodeMap[d]; ok { 212 | i.Ino = d.Ino 213 | return i, d.Name != i.Name 214 | } 215 | 216 | i.Ino = r.inumber 217 | r.inumber++ 218 | r.inodeMap[d] = i 219 | 220 | return i, false 221 | } 222 | 223 | // GetRecord returns a cpio Record for the given path on the local file system. 224 | // 225 | // GetRecord does not follow symlinks. If path is a symlink, the record 226 | // returned will reflect that symlink. 227 | func (r *Recorder) GetRecord(path string) (Record, error) { 228 | fi, err := os.Lstat(path) 229 | if err != nil { 230 | return Record{}, err 231 | } 232 | 233 | sys := fi.Sys().(*syscall.Stat_t) 234 | info, done := r.inode(sysInfo(path, sys)) 235 | 236 | switch fi.Mode() & os.ModeType { 237 | case 0: // Regular file. 238 | if done { 239 | return Record{Info: info}, nil 240 | } 241 | return Record{Info: info, ReaderAt: uio.NewLazyLimitFile(path, int64(info.FileSize))}, nil 242 | 243 | case os.ModeSymlink: 244 | linkname, err := os.Readlink(path) 245 | if err != nil { 246 | return Record{}, err 247 | } 248 | return StaticRecord([]byte(linkname), info), nil 249 | 250 | default: 251 | return StaticRecord(nil, info), nil 252 | } 253 | } 254 | 255 | // NewRecorder creates a new Recorder. 256 | // 257 | // A recorder is a structure that contains variables used to calculate 258 | // file parameters such as inode numbers for a CPIO file. The life-time 259 | // of a Record structure is meant to be the same as the construction of a 260 | // single CPIO archive. Do not reuse between CPIOs if you don't know what 261 | // you're doing. 262 | func NewRecorder() *Recorder { 263 | return &Recorder{make(map[devInode]Info), 2} 264 | } 265 | -------------------------------------------------------------------------------- /cpio/fs_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import ( 8 | "fmt" 9 | "io" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "syscall" 14 | 15 | "github.com/u-root/mkuimage/cpio/internal/upath" 16 | "github.com/u-root/uio/uio" 17 | ) 18 | 19 | // A Recorder is a structure that contains variables used to calculate 20 | // file parameters such as inode numbers for a CPIO file. The life-time 21 | // of a Record structure is meant to be the same as the construction of a 22 | // single CPIO archive. Do not reuse between CPIOs if you don't know what 23 | // you're doing. 24 | type Recorder struct { 25 | inumber uint64 26 | } 27 | 28 | var modeMap = map[uint64]os.FileMode{ 29 | modeFile: 0, 30 | modeDir: os.ModeDir, 31 | } 32 | 33 | func unixModeToFileType(m uint64) (os.FileMode, error) { 34 | if t, ok := modeMap[m&modeTypeMask]; ok { 35 | return t, nil 36 | } 37 | return 0, fmt.Errorf("invalid file type %#o", m&modeTypeMask) 38 | } 39 | 40 | func toFileMode(r Record) os.FileMode { 41 | return os.FileMode(perm(r)) 42 | } 43 | 44 | // setModes sets the modes. 45 | func setModes(r Record) error { 46 | return os.Chmod(r.Name, toFileMode(r)&os.ModePerm) 47 | } 48 | 49 | func perm(r Record) uint32 { 50 | return uint32(r.Mode) & modePermissions 51 | } 52 | 53 | func dev(r Record) int { 54 | return int(r.Rmajor<<8 | r.Rminor) 55 | } 56 | 57 | // CreateFile creates a local file for f relative to the current working 58 | // directory. 59 | // 60 | // CreateFile will attempt to set all metadata for the file, including 61 | // ownership, times, and permissions. 62 | func CreateFile(f Record) error { 63 | return CreateFileInRoot(f, ".", true) 64 | } 65 | 66 | // CreateFileInRoot creates a local file for f relative to rootDir. 67 | func CreateFileInRoot(f Record, rootDir string, forcePriv bool) error { 68 | m, err := unixModeToFileType(f.Mode) 69 | if err != nil { 70 | return err 71 | } 72 | 73 | f.Name, err = upath.SafeFilepathJoin(rootDir, f.Name) 74 | if err != nil { 75 | // The behavior is to skip files which are unsafe due to 76 | // zipslip, but continue extracting everything else. 77 | log.Printf("Warning: Skipping file %q due to: %v", f.Name, err) 78 | return nil 79 | } 80 | dir := filepath.Dir(f.Name) 81 | // The problem: many cpio archives do not specify the directories and 82 | // hence the permissions. They just specify the whole path. In order 83 | // to create files in these directories, we have to make them at least 84 | // mode 755. 85 | if _, err := os.Stat(dir); os.IsNotExist(err) && len(dir) > 0 { 86 | if err := os.MkdirAll(dir, 0o755); err != nil { 87 | return fmt.Errorf("CreateFileInRoot %q: %v", f.Name, err) 88 | } 89 | } 90 | 91 | switch m { 92 | case os.FileMode(0): 93 | nf, err := os.Create(f.Name) 94 | if err != nil { 95 | return err 96 | } 97 | defer nf.Close() 98 | if _, err := io.Copy(nf, uio.Reader(f)); err != nil { 99 | return err 100 | } 101 | 102 | case os.ModeDir: 103 | if err := os.MkdirAll(f.Name, toFileMode(f)); err != nil { 104 | return err 105 | } 106 | 107 | default: 108 | return fmt.Errorf("%v: Unknown type %#o", f.Name, m) 109 | } 110 | 111 | if err := setModes(f); err != nil && forcePriv { 112 | return err 113 | } 114 | return nil 115 | } 116 | 117 | func (r *Recorder) inode(i Info) Info { 118 | i.Ino = r.inumber 119 | r.inumber++ 120 | return i 121 | } 122 | 123 | // GetRecord returns a cpio Record for the given path on the local file system. 124 | // 125 | // GetRecord does not follow symlinks. If path is a symlink, the record 126 | // returned will reflect that symlink. 127 | func (r *Recorder) GetRecord(path string) (Record, error) { 128 | fi, err := os.Stat(path) 129 | if err != nil { 130 | return Record{}, err 131 | } 132 | sys, ok := fi.Sys().(*syscall.Win32FileAttributeData) 133 | if !ok { 134 | return Record{}, fmt.Errorf("sys is empty:%w", syscall.ENOSYS) 135 | } 136 | info := r.inode(sysInfo(path, sys)) 137 | 138 | switch fi.Mode() & os.ModeType { 139 | case 0: // Regular file. 140 | return Record{Info: info, ReaderAt: uio.NewLazyFile(path)}, nil 141 | default: 142 | return StaticRecord(nil, info), nil 143 | } 144 | } 145 | 146 | // NewRecorder creates a new Recorder. 147 | // 148 | // A recorder is a structure that contains variables used to calculate 149 | // file parameters such as inode numbers for a CPIO file. The life-time 150 | // of a Record structure is meant to be the same as the construction of a 151 | // single CPIO archive. Do not reuse between CPIOs if you don't know what 152 | // you're doing. 153 | func NewRecorder() *Recorder { 154 | return &Recorder{inumber: 2} 155 | } 156 | -------------------------------------------------------------------------------- /cpio/internal/upath/safejoin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 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 upath allows safely joining filepaths together without breaking the 6 | // boundary of a "chroot". 7 | package upath 8 | 9 | import ( 10 | "fmt" 11 | "path/filepath" 12 | "strings" 13 | ) 14 | 15 | // SafeFilepathJoin safely joins two paths path1+path2. The resulting path will 16 | // always be contained within path1 even if path2 tries to escape with "../". 17 | // If that path is not possible, an error is returned. The resulting path is 18 | // cleaned. 19 | func SafeFilepathJoin(path1, path2 string) (string, error) { 20 | relPath, err := filepath.Rel(".", path2) 21 | if err != nil || strings.HasPrefix(relPath, "..") { 22 | return "", fmt.Errorf("(zipslip) filepath is unsafe %q: %v", path2, err) 23 | } 24 | if path1 == "" { 25 | path1 = "." 26 | } 27 | return filepath.Join(path1, filepath.Join(string(filepath.Separator), relPath)), nil 28 | } 29 | -------------------------------------------------------------------------------- /cpio/internal/upath/safejoin_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 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 upath 6 | 7 | import ( 8 | "testing" 9 | ) 10 | 11 | func TestSafeFilepathJoin(t *testing.T) { 12 | for _, tt := range []struct { 13 | name string 14 | path1, path2 string 15 | wantPath string 16 | wantErr bool 17 | }{ 18 | { 19 | name: "safe relative paths", 20 | path1: "a", 21 | path2: "b", 22 | wantPath: "a/b", 23 | }, 24 | { 25 | name: "safe relative paths 2", 26 | path1: "./a", 27 | path2: "./b", 28 | wantPath: "a/b", 29 | }, 30 | { 31 | name: "unsafe absolute paths", 32 | path1: "/a", 33 | path2: "/b", 34 | wantErr: true, 35 | }, 36 | { 37 | name: "safe absolute path", 38 | path1: "/a", 39 | path2: "b", 40 | wantPath: "/a/b", 41 | }, 42 | { 43 | name: "unsafe absolute path", 44 | path1: "a", 45 | path2: "/b", 46 | wantErr: true, 47 | }, 48 | { 49 | name: "unsafe dotdot escape", 50 | path1: "/a", 51 | path2: "../b", 52 | wantErr: true, 53 | }, 54 | { 55 | name: "unsafe dotdot escape 2", 56 | path1: "/a", 57 | path2: "c/d/../../../b", 58 | wantErr: true, 59 | }, 60 | { 61 | name: "unsafe dotdot escape 3", 62 | path1: "/a", 63 | path2: "c/d/../../../a/b", 64 | wantErr: true, 65 | }, 66 | { 67 | name: "safe dotdot", 68 | path1: "/a", 69 | path2: "c/../b", 70 | wantPath: "/a/b", 71 | }, 72 | { 73 | name: "safe dotdot 2", 74 | path1: "/a", 75 | path2: "c/d/../b", 76 | wantPath: "/a/c/b", 77 | }, 78 | { 79 | name: "safe dotdot 3", 80 | path1: "../a", 81 | path2: "c/d/../b", 82 | wantPath: "../a/c/b", 83 | }, 84 | { 85 | name: "safe missing path", 86 | path1: "", 87 | path2: "b", 88 | wantPath: "b", 89 | }, 90 | { 91 | name: "safe missing path 2", 92 | path1: "a", 93 | path2: "", 94 | wantPath: "a", 95 | }, 96 | } { 97 | t.Run(tt.name, func(t *testing.T) { 98 | got, err := SafeFilepathJoin(tt.path1, tt.path2) 99 | if (err == nil) == tt.wantErr { 100 | t.Errorf("safePathJoin(%q, %q) = err %v; wantErr=%v", tt.path1, tt.path2, err, tt.wantErr) 101 | } 102 | if got != tt.wantPath { 103 | t.Errorf("safePathJoin(%q, %q) = %q; want %q", tt.path1, tt.path2, got, tt.wantPath) 104 | } 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /cpio/mknod_freebsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2019 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 cpio 6 | 7 | import ( 8 | "syscall" 9 | ) 10 | 11 | func mknod(path string, mode uint32, dev int) (err error) { 12 | return syscall.Mknod(path, mode, uint64(dev)) 13 | } 14 | -------------------------------------------------------------------------------- /cpio/mknod_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2019 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 | //go:build !freebsd && !plan9 && !windows 6 | // +build !freebsd,!plan9,!windows 7 | 8 | package cpio 9 | 10 | import ( 11 | "syscall" 12 | ) 13 | 14 | func mknod(path string, mode uint32, dev int) (err error) { 15 | return syscall.Mknod(path, mode, dev) 16 | } 17 | -------------------------------------------------------------------------------- /cpio/newc.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import ( 8 | "bytes" 9 | "encoding/binary" 10 | "encoding/hex" 11 | "fmt" 12 | "io" 13 | "os" 14 | 15 | "github.com/u-root/uio/uio" 16 | ) 17 | 18 | const ( 19 | newcMagic = "070701" 20 | magicLen = 6 21 | ) 22 | 23 | // Newc is the newc CPIO record format. 24 | var Newc RecordFormat = newc{magic: newcMagic} 25 | 26 | type header struct { 27 | Ino uint32 28 | Mode uint32 29 | UID uint32 30 | GID uint32 31 | NLink uint32 32 | MTime uint32 33 | FileSize uint32 34 | Major uint32 35 | Minor uint32 36 | Rmajor uint32 37 | Rminor uint32 38 | NameLength uint32 39 | CRC uint32 40 | } 41 | 42 | func headerFromInfo(i Info) header { 43 | var h header 44 | h.Ino = uint32(i.Ino) 45 | h.Mode = uint32(i.Mode) 46 | h.UID = uint32(i.UID) 47 | h.GID = uint32(i.GID) 48 | h.NLink = uint32(i.NLink) 49 | h.MTime = uint32(i.MTime) 50 | h.FileSize = uint32(i.FileSize) 51 | h.Major = uint32(i.Major) 52 | h.Minor = uint32(i.Minor) 53 | h.Rmajor = uint32(i.Rmajor) 54 | h.Rminor = uint32(i.Rminor) 55 | h.NameLength = uint32(len(i.Name)) + 1 56 | return h 57 | } 58 | 59 | func (h header) info() Info { 60 | var i Info 61 | i.Ino = uint64(h.Ino) 62 | i.Mode = uint64(h.Mode) 63 | i.UID = uint64(h.UID) 64 | i.GID = uint64(h.GID) 65 | i.NLink = uint64(h.NLink) 66 | i.MTime = uint64(h.MTime) 67 | i.FileSize = uint64(h.FileSize) 68 | i.Major = uint64(h.Major) 69 | i.Minor = uint64(h.Minor) 70 | i.Rmajor = uint64(h.Rmajor) 71 | i.Rminor = uint64(h.Rminor) 72 | return i 73 | } 74 | 75 | // newc implements RecordFormat for the newc format. 76 | type newc struct { 77 | magic string 78 | } 79 | 80 | // round4 returns the next multiple of 4 close to n. 81 | func round4(n int64) int64 { 82 | return (n + 3) &^ 0x3 83 | } 84 | 85 | type writer struct { 86 | n newc 87 | w io.Writer 88 | pos int64 89 | } 90 | 91 | // Writer implements RecordFormat.Writer. 92 | func (n newc) Writer(w io.Writer) RecordWriter { 93 | return NewDedupWriter(&writer{n: n, w: w}) 94 | } 95 | 96 | func (w *writer) Write(b []byte) (int, error) { 97 | n, err := w.w.Write(b) 98 | if err != nil { 99 | return 0, err 100 | } 101 | w.pos += int64(n) 102 | return n, nil 103 | } 104 | 105 | func (w *writer) pad() error { 106 | if o := round4(w.pos); o != w.pos { 107 | var pad [3]byte 108 | if _, err := w.Write(pad[:o-w.pos]); err != nil { 109 | return err 110 | } 111 | } 112 | return nil 113 | } 114 | 115 | // WriteRecord writes newc cpio records. It pads the header+name write to 4 116 | // byte alignment and pads the data write as well. 117 | func (w *writer) WriteRecord(f Record) error { 118 | // Write magic. 119 | if _, err := w.Write([]byte(w.n.magic)); err != nil { 120 | return err 121 | } 122 | 123 | buf := &bytes.Buffer{} 124 | hdr := headerFromInfo(f.Info) 125 | if f.ReaderAt == nil { 126 | hdr.FileSize = 0 127 | } 128 | hdr.CRC = 0 129 | if err := binary.Write(buf, binary.BigEndian, hdr); err != nil { 130 | return err 131 | } 132 | 133 | hexBuf := make([]byte, hex.EncodedLen(buf.Len())) 134 | n := hex.Encode(hexBuf, buf.Bytes()) 135 | // It's much easier to debug if we match GNU output format. 136 | hexBuf = bytes.ToUpper(hexBuf) 137 | 138 | // Write header. 139 | if _, err := w.Write(hexBuf[:n]); err != nil { 140 | return err 141 | } 142 | 143 | // Append NULL char. 144 | cstr := append([]byte(f.Info.Name), 0) 145 | // Write name. 146 | if _, err := w.Write(cstr); err != nil { 147 | return err 148 | } 149 | 150 | // Pad to a multiple of 4. 151 | if err := w.pad(); err != nil { 152 | return err 153 | } 154 | 155 | // Some files do not have any content. 156 | if f.ReaderAt == nil { 157 | return nil 158 | } 159 | 160 | // Write file contents. 161 | m, err := io.Copy(w, uio.Reader(f)) 162 | if err != nil { 163 | return err 164 | } 165 | if m != int64(f.Info.FileSize) { 166 | return fmt.Errorf("WriteRecord: %s: wrote %d bytes of file instead of %d bytes; archive is now corrupt", f.Info.Name, m, f.Info.FileSize) 167 | } 168 | if c, ok := f.ReaderAt.(io.Closer); ok { 169 | if err := c.Close(); err != nil { 170 | return err 171 | } 172 | } 173 | if m > 0 { 174 | return w.pad() 175 | } 176 | return nil 177 | } 178 | 179 | type reader struct { 180 | n newc 181 | r io.ReaderAt 182 | pos int64 183 | } 184 | 185 | // discarder is used to implement ReadAt from a Reader 186 | // by reading, and discarding, data until the offset 187 | // is reached. It can only go forward. It is designed 188 | // for pipe-like files. 189 | type discarder struct { 190 | r io.Reader 191 | pos int64 192 | } 193 | 194 | // ReadAt implements ReadAt for a discarder. 195 | // It is an error for the offset to be negative. 196 | func (r *discarder) ReadAt(p []byte, off int64) (int, error) { 197 | if off-r.pos < 0 { 198 | return 0, fmt.Errorf("negative seek on discarder not allowed") 199 | } 200 | if off != r.pos { 201 | i, err := io.Copy(io.Discard, io.LimitReader(r.r, off-r.pos)) 202 | if err != nil || i != off-r.pos { 203 | return 0, err 204 | } 205 | r.pos += i 206 | } 207 | n, err := io.ReadFull(r.r, p) 208 | if err != nil { 209 | return n, err 210 | } 211 | r.pos += int64(n) 212 | return n, err 213 | } 214 | 215 | var _ io.ReaderAt = &discarder{} 216 | 217 | // Reader implements RecordFormat.Reader. 218 | func (n newc) Reader(r io.ReaderAt) RecordReader { 219 | return EOFReader{&reader{n: n, r: r}} 220 | } 221 | 222 | // FileReader implements RecordFormat.Reader. If the file 223 | // implements ReadAt, then it is used for greater efficiency. 224 | // If it only implements Read, then a discarder will be used 225 | // instead. 226 | // 227 | // Note a complication: 228 | // 229 | // r, _, _ := os.Pipe() 230 | // var b [2]byte 231 | // _, err := r.ReadAt(b[:], 0) 232 | // fmt.Printf("%v", err) 233 | // 234 | // Pipes claim to implement ReadAt; most Unix kernels 235 | // do not agree. Even a seek to the current position fails. 236 | // This means that 237 | // if rat, ok := r.(io.ReaderAt); ok { 238 | // would seem to work, but would fail when the 239 | // actual ReadAt on the pipe occurs, even for offset 0, 240 | // which does not require a seek! The kernel checks for 241 | // whether the fd is seekable and returns an error, 242 | // even for values of offset which won't require a seek. 243 | // So, the code makes a simple test: can we seek to 244 | // current offset? If not, then the file is wrapped with a 245 | // discardreader. The discard reader is far less efficient 246 | // but allows cpio to read from a pipe. 247 | func (n newc) FileReader(f *os.File) RecordReader { 248 | _, err := f.Seek(0, 0) 249 | if err == nil { 250 | return EOFReader{&reader{n: n, r: f}} 251 | } 252 | return EOFReader{&reader{n: n, r: &discarder{r: f}}} 253 | } 254 | 255 | func (r *reader) read(p []byte) error { 256 | n, err := r.r.ReadAt(p, r.pos) 257 | 258 | if err == io.EOF { 259 | return io.EOF 260 | } 261 | 262 | if err != nil || n != len(p) { 263 | return fmt.Errorf("ReadAt(pos = %d): got %d, want %d bytes; error %v", r.pos, n, len(p), err) 264 | } 265 | 266 | r.pos += int64(n) 267 | return nil 268 | } 269 | 270 | func (r *reader) readAligned(p []byte) error { 271 | err := r.read(p) 272 | r.pos = round4(r.pos) 273 | return err 274 | } 275 | 276 | // ReadRecord implements RecordReader for the newc cpio format. 277 | func (r *reader) ReadRecord() (Record, error) { 278 | hdr := header{} 279 | recPos := r.pos 280 | 281 | buf := make([]byte, hex.EncodedLen(binary.Size(hdr))+magicLen) 282 | if err := r.read(buf); err != nil { 283 | return Record{}, err 284 | } 285 | 286 | // Check the magic. 287 | if magic := string(buf[:magicLen]); magic != r.n.magic { 288 | return Record{}, fmt.Errorf("reader: magic got %q, want %q", magic, r.n.magic) 289 | } 290 | 291 | // Decode hex header fields. 292 | dst := make([]byte, binary.Size(hdr)) 293 | if _, err := hex.Decode(dst, buf[magicLen:]); err != nil { 294 | return Record{}, fmt.Errorf("reader: error decoding hex: %v", err) 295 | } 296 | if err := binary.Read(bytes.NewReader(dst), binary.BigEndian, &hdr); err != nil { 297 | return Record{}, err 298 | } 299 | Debug("Decoded header is %v\n", hdr) 300 | 301 | // Get the name. 302 | if hdr.NameLength == 0 { 303 | return Record{}, fmt.Errorf("name field of length zero") 304 | } 305 | nameBuf := make([]byte, hdr.NameLength) 306 | if err := r.readAligned(nameBuf); err != nil { 307 | Debug("name read failed") 308 | return Record{}, err 309 | } 310 | 311 | info := hdr.info() 312 | info.Name = Normalize(string(nameBuf[:hdr.NameLength-1])) 313 | 314 | recLen := uint64(r.pos - recPos) 315 | filePos := r.pos 316 | 317 | //TODO: check if hdr.FileSize is equal to the actual fileSize of the record 318 | content := io.NewSectionReader(r.r, r.pos, int64(hdr.FileSize)) 319 | r.pos = round4(r.pos + int64(hdr.FileSize)) 320 | return Record{ 321 | Info: info, 322 | ReaderAt: content, 323 | RecLen: recLen, 324 | RecPos: recPos, 325 | FilePos: filePos, 326 | }, nil 327 | } 328 | 329 | func init() { 330 | formatMap["newc"] = Newc 331 | } 332 | -------------------------------------------------------------------------------- /cpio/sysinfo_darwin.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import ( 8 | "syscall" 9 | ) 10 | 11 | func sysInfo(n string, sys *syscall.Stat_t) Info { 12 | return Info{ 13 | Ino: sys.Ino, 14 | Mode: uint64(sys.Mode), 15 | UID: uint64(sys.Uid), 16 | GID: uint64(sys.Gid), 17 | NLink: uint64(sys.Nlink), 18 | MTime: uint64(sys.Mtimespec.Sec), 19 | FileSize: uint64(sys.Size), 20 | Dev: uint64(sys.Dev), 21 | Major: uint64(sys.Dev >> 8), 22 | Minor: uint64(sys.Dev & 0xff), 23 | Rmajor: uint64(sys.Rdev >> 8), 24 | Rminor: uint64(sys.Rdev & 0xff), 25 | Name: n, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /cpio/sysinfo_freebsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2019 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 cpio 6 | 7 | import ( 8 | "syscall" 9 | ) 10 | 11 | func sysInfo(n string, sys *syscall.Stat_t) Info { 12 | return Info{ 13 | Ino: sys.Ino, 14 | Mode: uint64(sys.Mode), 15 | UID: uint64(sys.Uid), 16 | GID: uint64(sys.Gid), 17 | NLink: sys.Nlink, 18 | FileSize: uint64(sys.Size), 19 | Major: sys.Dev >> 8, 20 | Minor: sys.Dev & 0xff, 21 | Rmajor: sys.Rdev >> 8, 22 | Rminor: sys.Rdev & 0xff, 23 | Name: n, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cpio/sysinfo_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import ( 8 | "syscall" 9 | ) 10 | 11 | func sysInfo(n string, sys *syscall.Stat_t) Info { 12 | //nolint:unconvert 13 | return Info{ 14 | Ino: sys.Ino, 15 | Mode: uint64(sys.Mode), 16 | UID: uint64(sys.Uid), 17 | GID: uint64(sys.Gid), 18 | NLink: uint64(sys.Nlink), 19 | MTime: uint64(sys.Mtim.Sec), 20 | FileSize: uint64(sys.Size), 21 | Dev: uint64(sys.Dev), 22 | Major: uint64(sys.Dev >> 8), 23 | Minor: uint64(sys.Dev & 0xff), 24 | Rmajor: uint64(sys.Rdev >> 8), 25 | Rminor: uint64(sys.Rdev & 0xff), 26 | Name: n, 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /cpio/sysinfo_plan9.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import "syscall" 8 | 9 | func sysInfo(n string, sys *syscall.Dir) Info { 10 | // Similar to how the standard library converts Plan 9 Dir to os.FileInfo: 11 | // https://github.com/golang/go/blob/go1.16beta1/src/os/stat_plan9.go#L14 12 | mode := sys.Mode & 0o777 13 | if sys.Mode&syscall.DMDIR != 0 { 14 | mode |= modeDir 15 | } else { 16 | mode |= modeFile 17 | } 18 | return Info{ 19 | Mode: uint64(mode), 20 | UID: 0, 21 | MTime: uint64(sys.Mtime), 22 | FileSize: uint64(sys.Length), 23 | Name: n, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cpio/sysinfo_windows.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import ( 8 | "syscall" 9 | ) 10 | 11 | func sysInfo(n string, sys *syscall.Win32FileAttributeData) Info { 12 | sz := uint64(sys.FileSizeHigh)<<32 | uint64(sys.FileSizeLow) 13 | mtime := uint64(sys.CreationTime.Nanoseconds()) / 1_000_000_000 14 | 15 | return Info{ 16 | Ino: 0, 17 | Mode: uint64(sys.FileAttributes), 18 | UID: uint64(0), 19 | GID: uint64(0), 20 | NLink: uint64(1), 21 | MTime: mtime, 22 | FileSize: sz, 23 | Dev: 0, 24 | Major: 0, 25 | Minor: 0, 26 | Rmajor: 0, 27 | Rminor: 0, 28 | Name: n, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /cpio/testdata/fuzz/corpora/archive1.cpio: -------------------------------------------------------------------------------- 1 | 0707071764010027001006640017500017500000010000001430605041400002000000000007/tmp/test/test3foobar 2 | 0707071764010027031006640017500017500000010000001430605043600002000000000016/tmp/test/test4foobar=barfoo 3 | 0707071764010027311006640017500017500000010000001430605044000002000000000003/tmp/test/test5ab 4 | 0707071764010026601006640017500017500000010000001430605036300001700000000004/tmp/test/testfoo 5 | 0707071764010026641006640017500017500000010000001430605040300002000000000013/tmp/test/test2foobartest 6 | 0707071764010026200407750017500017500000020000001430605067200001300000000000/tmp/test/0707070000000000000000000000000000000000010000000000000000000001300000000000TRAILER!!! -------------------------------------------------------------------------------- /cpio/testdata/fuzz/corpora/archive2.cpio: -------------------------------------------------------------------------------- 1 | 0707071764010027001006640017500017500000010000001430605041400002000000000007/tmp/test/test3foobar 2 | 0707071764010035441006640017500017500000010000001430605125200002500000000026/tmp/test/newfiletootestingpurpose=foobar 3 | 0707071764010015021006640017500017500000010000001430605123700002200000000017/tmp/test/newfiletestingpurpose 4 | 0707071764010027031006640017500017500000010000001430605043600002000000000016/tmp/test/test4foobar=barfoo 5 | 0707071764010027311006640017500017500000010000001430605044000002000000000003/tmp/test/test5ab 6 | 0707071764010015011006640017500017500000010000001430605112700002100000000005/tmp/test/test23test 7 | 0707071764010025151006640017500017500000010000001430605115600002100000000022/tmp/test/abc23ffoobar=barfoo=NaN 8 | 0707071764010026601006640017500017500000010000001430605036300001700000000004/tmp/test/testfoo 9 | 0707071764010026641006640017500017500000010000001430605040300002000000000013/tmp/test/test2foobartest 10 | 0707071764010026200407750017500017500000020000001430605125200001300000000000/tmp/test/0707070000000000000000000000000000000000010000000000000000000001300000000000TRAILER!!! -------------------------------------------------------------------------------- /cpio/testdata/fuzz/corpora/archive3.cpio: -------------------------------------------------------------------------------- 1 | 0707071764010027001006640017500017500000010000001430605041400002000000000007/tmp/test/test3foobar 2 | 0707071764010035441006640017500017500000010000001430605125200002500000000026/tmp/test/newfiletootestingpurpose=foobar 3 | 0707071764010015021006640017500017500000010000001430605123700002200000000017/tmp/test/newfiletestingpurpose 4 | 0707071764010027031006640017500017500000010000001430605043600002000000000016/tmp/test/test4foobar=barfoo 5 | 0707071764010027311006640017500017500000010000001430605044000002000000000003/tmp/test/test5ab 6 | 0707071764010015011006640017500017500000010000001430605112700002100000000005/tmp/test/test23test 7 | 0707071764010025151006640017500017500000010000001430605115600002100000000022/tmp/test/abc23ffoobar=barfoo=NaN 8 | 0707071764010026601006640017500017500000010000001430605036300001700000000004/tmp/test/testfoo 9 | 0707071764010026641006640017500017500000010000001430605040300002000000000013/tmp/test/test2foobartest 10 | 0707071764010026200407750017500017500000020000001430605125200001300000000000/tmp/test/0707070000000000000000000000000000000000010000000000000000000001300000000000TRAILER!!! -------------------------------------------------------------------------------- /cpio/testdata/fuzz/corpora/archive4.cpio: -------------------------------------------------------------------------------- 1 | 0707071764010027031006640017500017500000010000001430605043600002000000000016/tmp/test/test4foobar=barfoo 2 | 0707071764010027311006640017500017500000010000001430605044000002000000000003/tmp/test/test5ab 3 | 0707071764010015011006640017500017500000010000001430605112700002100000000005/tmp/test/test23test 4 | 0707071764010025151006640017500017500000010000001430605115600002100000000022/tmp/test/abc23ffoobar=barfoo=NaN 5 | 0707071764010026601006640017500017500000010000001430605036300001700000000004/tmp/test/testfoo 6 | 0707071764010026200407750017500017500000020000001430605131000001300000000000/tmp/test/0707070000000000000000000000000000000000010000000000000000000001300000000000TRAILER!!! -------------------------------------------------------------------------------- /cpio/testdata/fuzz/corpora/archive5.cpio: -------------------------------------------------------------------------------- 1 | 0707071764010026641006640017500017500000010000001430605134600001700000000000/tmp/temp/file0707071764010015020407750017500017500000020000001430605134600001300000000000/tmp/temp/0707070000000000000000000000000000000000010000000000000000000001300000000000TRAILER!!! -------------------------------------------------------------------------------- /cpio/testdata/fuzz/cpio.dict: -------------------------------------------------------------------------------- 1 | # libfuzzer dictionary used for fuzzing cpio 2 | name="file" 3 | newc_magic="\x07\x07\x01" 4 | fileModeIEXEC="\x40" 5 | fileModeIFBLK="\x60\x00" 6 | fileModeIFCHR="\x20\x00" 7 | fileModeIFDIR="\x40\x00" 8 | fileModeIFIFO="\x10\x00" 9 | fileModeIFLNK="\xa0\x00" 10 | fileModeIFMT"\xf0\x00" 11 | fileModeIFREG="\x80\x00" 12 | fileModeIFSOCK="\xc0\x00" 13 | fileModeIFWHT="\xe0\x00" 14 | fileModeIREAD="\x01\x00" 15 | fileModeIGRP="\x20" 16 | fileModeIROTH="\x04" 17 | fileModeIRUSR="\x01\x00" 18 | fileModeIRWXG="\x38" 19 | fileModeIRWO="\x07" 20 | fileModeIRWXU="\x01\xc0" 21 | fileModeISGID="\x04\x00" 22 | fileModeISTXT="\x02\x00" 23 | fileModeISUID="\x08\x00" 24 | fileModeISVTX="\x02\x00" 25 | -------------------------------------------------------------------------------- /cpio/testdata/fuzz/fuzz_read_write_newc.options: -------------------------------------------------------------------------------- 1 | [libfuzzer] 2 | dict = cpio.dict 3 | max_len=64 -------------------------------------------------------------------------------- /cpio/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import ( 8 | "bytes" 9 | "fmt" 10 | "io" 11 | "os" 12 | "path" 13 | "strings" 14 | 15 | "github.com/u-root/uio/uio" 16 | ) 17 | 18 | // Trailer is the name of the trailer record. 19 | const Trailer = "TRAILER!!!" 20 | 21 | // TrailerRecord is the last record in any CPIO archive. 22 | var TrailerRecord = StaticRecord(nil, Info{Name: Trailer}) 23 | 24 | // StaticRecord returns a record with the given contents and metadata. 25 | func StaticRecord(contents []byte, info Info) Record { 26 | info.FileSize = uint64(len(contents)) 27 | return Record{ 28 | ReaderAt: bytes.NewReader(contents), 29 | Info: info, 30 | } 31 | } 32 | 33 | // StaticFile returns a normal file record. 34 | func StaticFile(name string, content string, perm uint64) Record { 35 | return StaticRecord([]byte(content), Info{ 36 | Name: name, 37 | Mode: S_IFREG | perm, 38 | }) 39 | } 40 | 41 | // Symlink returns a symlink record at name pointing to target. 42 | func Symlink(name string, target string) Record { 43 | return Record{ 44 | ReaderAt: strings.NewReader(target), 45 | Info: Info{ 46 | FileSize: uint64(len(target)), 47 | Mode: S_IFLNK | 0o777, 48 | Name: name, 49 | }, 50 | } 51 | } 52 | 53 | // Directory returns a directory record at name. 54 | func Directory(name string, mode uint64) Record { 55 | return Record{ 56 | Info: Info{ 57 | Name: name, 58 | Mode: S_IFDIR | mode&^S_IFMT, 59 | }, 60 | } 61 | } 62 | 63 | // CharDev returns a character device record at name. 64 | func CharDev(name string, perm uint64, rmajor, rminor uint64) Record { 65 | return Record{ 66 | Info: Info{ 67 | Name: name, 68 | Mode: S_IFCHR | perm, 69 | Rmajor: rmajor, 70 | Rminor: rminor, 71 | }, 72 | } 73 | } 74 | 75 | // EOFReader is a RecordReader that converts the Trailer record to io.EOF. 76 | type EOFReader struct { 77 | RecordReader 78 | } 79 | 80 | // ReadRecord implements RecordReader. 81 | // 82 | // ReadRecord returns io.EOF when the record name is TRAILER!!!. 83 | func (r EOFReader) ReadRecord() (Record, error) { 84 | rec, err := r.RecordReader.ReadRecord() 85 | if err != nil { 86 | return Record{}, err 87 | } 88 | // The end of a CPIO archive is marked by a record whose name is 89 | // "TRAILER!!!". 90 | if rec.Name == Trailer { 91 | return Record{}, io.EOF 92 | } 93 | return rec, nil 94 | } 95 | 96 | // DedupWriter is a RecordWriter that does not write more than one record with 97 | // the same path. 98 | // 99 | // There seems to be no harm done in stripping duplicate names when the record 100 | // is written, and lots of harm done if we don't do it. 101 | type DedupWriter struct { 102 | rw RecordWriter 103 | 104 | // alreadyWritten keeps track of paths already written to rw. 105 | alreadyWritten map[string]struct{} 106 | } 107 | 108 | // NewDedupWriter returns a new deduplicating rw. 109 | func NewDedupWriter(rw RecordWriter) RecordWriter { 110 | return &DedupWriter{ 111 | rw: rw, 112 | alreadyWritten: make(map[string]struct{}), 113 | } 114 | } 115 | 116 | // WriteRecord implements RecordWriter. 117 | // 118 | // If rec.Name was already seen once before, it will not be written again and 119 | // WriteRecord returns nil. 120 | func (dw *DedupWriter) WriteRecord(rec Record) error { 121 | rec.Name = Normalize(rec.Name) 122 | 123 | if _, ok := dw.alreadyWritten[rec.Name]; ok { 124 | return nil 125 | } 126 | dw.alreadyWritten[rec.Name] = struct{}{} 127 | return dw.rw.WriteRecord(rec) 128 | } 129 | 130 | // WriteRecords writes multiple records to w. 131 | func WriteRecords(w RecordWriter, files []Record) error { 132 | for _, f := range files { 133 | if err := w.WriteRecord(f); err != nil { 134 | return fmt.Errorf("WriteRecords: writing %q got %v", f.Info.Name, err) 135 | } 136 | } 137 | return nil 138 | } 139 | 140 | // WriteRecordsAndDirs writes records to w, with a slight difference from WriteRecords: 141 | // the record path is split and all the 142 | // directories are written first, in order, mimic'ing what happens with 143 | // find . -print 144 | // 145 | // When is this function needed? 146 | // Most cpio programs will create directories as needed for paths such as a/b/c/d 147 | // The cpio creation process for Linux uses find, and will create a 148 | // record for each directory in a/b/c/d 149 | // 150 | // But when code programatically generates a cpio for the Linux kernel, 151 | // the cpio is not generated via find, and Linux will not create 152 | // intermediate directories. The result, seen in practice, is that a path, 153 | // such as a/b/c/d, when unpacked by the linux kernel, will be ignored if 154 | // a/b/c does not exist! 155 | // 156 | // Again, this function is very rarely needed, save when we programatically generate 157 | // an initramfs for Linux. 158 | // This code only works with a deduplicating writer. Further, it will not accept a 159 | // Record if the full pathname of that Record already exists. This is arguably 160 | // overly restrictive but, at the same, avoids some very unpleasant programmer 161 | // errors. 162 | // There is overlap here with DedupWriter but given that this is a Special Snowflake 163 | // function, it seems best to leave the DedupWriter code alone. 164 | func WriteRecordsAndDirs(rw RecordWriter, files []Record) error { 165 | w, ok := rw.(*DedupWriter) 166 | if !ok { 167 | return fmt.Errorf("WriteRecordsAndDirs(%T,...): only DedupWriter allowed:%w", rw, os.ErrInvalid) 168 | } 169 | for _, f := range files { 170 | // This redundant Normalize does no harm, but, yes, it is redundant. 171 | // Signed 172 | // The Department of Redundancy Department. 173 | f.Name = Normalize(f.Name) 174 | if r, ok := w.alreadyWritten[f.Name]; ok { 175 | return fmt.Errorf("WriteRecordsAndDirs: %q already in the archive: %v:%w", f.Name, r, os.ErrExist) 176 | } 177 | 178 | var recs []Record 179 | // Paths must be written to the archive in the order in which they 180 | // need to be created, i.e., a/b/c/d must be written as 181 | // a, a/b/, a/b/c, a/b/c/d 182 | // Note: do not use os.Separator here: cpio is a Unix standard, and hence 183 | // / is used. 184 | // do NOT use filepath, use path for the same reason. 185 | // Things you learn the hard way when you run on Windows. 186 | els := strings.Split(path.Dir(f.Name), "/") 187 | for i := range els { 188 | d := path.Join(els[:i+1]...) 189 | recs = append(recs, Directory(d, 0777)) 190 | } 191 | recs = append(recs, f) 192 | if err := WriteRecords(rw, recs); err != nil { 193 | return fmt.Errorf("WriteRecords: writing %q got %v", f.Info.Name, err) 194 | } 195 | } 196 | return nil 197 | } 198 | 199 | // CopyAndFinish copies from a RecordReader to a RecordWriter. 200 | // 201 | // CopyAndFinish writes a trailer record. 202 | // 203 | // It processes one record at a time to minimize the memory footprint. 204 | func CopyAndFinish(w RecordWriter, r RecordReader) error { 205 | if err := Copy(w, r, nil); err != nil { 206 | return err 207 | } 208 | return WriteTrailer(w) 209 | } 210 | 211 | // WriteTrailer writes the trailer record. 212 | func WriteTrailer(w RecordWriter) error { 213 | return w.WriteRecord(TrailerRecord) 214 | } 215 | 216 | // Copy reads files from r one at a time, and writes them to w. 217 | // 218 | // Copy does not write a trailer record and applies transform to every record 219 | // before writing it. transform may be nil. 220 | func Copy(w RecordWriter, r RecordReader, transform func(Record) Record) error { 221 | return ForEachRecord(r, func(f Record) error { 222 | if transform != nil { 223 | f = transform(f) 224 | } 225 | return w.WriteRecord(f) 226 | }) 227 | } 228 | 229 | // ReadAllRecords returns all records in r in the order in which they were 230 | // read. 231 | func ReadAllRecords(rr RecordReader) ([]Record, error) { 232 | var files []Record 233 | err := ForEachRecord(rr, func(r Record) error { 234 | files = append(files, r) 235 | return nil 236 | }) 237 | return files, err 238 | } 239 | 240 | // ForEachRecord reads every record from r and applies f. 241 | func ForEachRecord(rr RecordReader, fun func(Record) error) error { 242 | for { 243 | rec, err := rr.ReadRecord() 244 | switch err { 245 | case io.EOF: 246 | return nil 247 | 248 | case nil: 249 | if err := fun(rec); err != nil { 250 | return err 251 | } 252 | 253 | default: 254 | return err 255 | } 256 | } 257 | } 258 | 259 | // Normalize normalizes namepath to be relative to /. 260 | func Normalize(name string) string { 261 | // do not use filepath.IsAbs, it will not work on Windows. 262 | // do not use filepath.Rel, that will not work 263 | // sensibly on windows. 264 | // The only thing one can do is strip all leading 265 | // / 266 | name = strings.TrimLeft(name, "/") 267 | // do not use filepath.Clean here. 268 | // This will result in paths with \\ on windows, and 269 | // / is the cpio standard. 270 | return path.Clean(name) 271 | } 272 | 273 | // MakeReproducible changes any fields in a Record such that if we run cpio 274 | // again, with the same files presented to it in the same order, and those 275 | // files have unchanged contents, the cpio file it produces will be bit-for-bit 276 | // identical. This is an essential property for firmware-embedded payloads. 277 | func MakeReproducible(r Record) Record { 278 | r.Ino = 0 279 | r.Name = Normalize(r.Name) 280 | r.MTime = 0 281 | r.UID = 0 282 | r.GID = 0 283 | r.Dev = 0 284 | r.Major = 0 285 | r.Minor = 0 286 | r.NLink = 0 287 | return r 288 | } 289 | 290 | // MakeAllReproducible makes all given records reproducible as in 291 | // MakeReproducible. 292 | func MakeAllReproducible(files []Record) { 293 | for i := range files { 294 | files[i] = MakeReproducible(files[i]) 295 | } 296 | } 297 | 298 | // AllEqual compares all metadata and contents of r and s. 299 | func AllEqual(r []Record, s []Record) bool { 300 | if len(r) != len(s) { 301 | return false 302 | } 303 | for i := range r { 304 | if !Equal(r[i], s[i]) { 305 | return false 306 | } 307 | } 308 | return true 309 | } 310 | 311 | // Equal compares the metadata and contents of r and s. 312 | func Equal(r Record, s Record) bool { 313 | if r.Info != s.Info { 314 | return false 315 | } 316 | return uio.ReaderAtEqual(r.ReaderAt, s.ReaderAt) 317 | } 318 | -------------------------------------------------------------------------------- /cpio/utils_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2013-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 cpio 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "io/fs" 11 | "os" 12 | "testing" 13 | ) 14 | 15 | func TestNormalize(t *testing.T) { 16 | for _, tt := range []struct { 17 | path string 18 | want string 19 | }{ 20 | { 21 | path: "/foo/bar", 22 | want: "foo/bar", 23 | }, 24 | { 25 | path: "foo////bar", 26 | want: "foo/bar", 27 | }, 28 | { 29 | path: "/foo/bar/../baz", 30 | want: "foo/baz", 31 | }, 32 | { 33 | path: "foo/bar/../baz", 34 | want: "foo/baz", 35 | }, 36 | { 37 | path: "./foo/bar", 38 | want: "foo/bar", 39 | }, 40 | { 41 | path: "foo/../../bar", 42 | want: "../bar", 43 | }, 44 | { 45 | path: "", 46 | want: ".", 47 | }, 48 | { 49 | path: ".", 50 | want: ".", 51 | }, 52 | } { 53 | if got := Normalize(tt.path); got != tt.want { 54 | t.Errorf("Normalize(%q) = %q, want %q", tt.path, got, tt.want) 55 | } 56 | } 57 | } 58 | 59 | type bad struct { 60 | err error 61 | } 62 | 63 | func (b *bad) WriteRecord(_ Record) error { 64 | return b.err 65 | } 66 | 67 | var _ RecordWriter = &bad{} 68 | 69 | func TestWriteRecordsAndDirs(t *testing.T) { 70 | // Make sure it fails for the non DedupWriters 71 | if err := WriteRecordsAndDirs(&bad{}, nil); !errors.Is(err, os.ErrInvalid) { 72 | t.Errorf("WriteRecordsAndDirs(&bad{}, nil): got %v, want %v", err, os.ErrInvalid) 73 | } 74 | var paths = []struct { 75 | name string 76 | err error 77 | }{ 78 | {name: "a/b/c/d", err: nil}, 79 | {name: "a/b/c/e", err: nil}, 80 | {name: "a/b", err: nil}, 81 | } 82 | 83 | recs := make([]Record, 0) 84 | for _, p := range paths { 85 | recs = append(recs, Directory(p.name, 0777)) 86 | } 87 | var b bytes.Buffer 88 | w := Newc.Writer(&b) 89 | if err := WriteRecordsAndDirs(w, recs[:2]); err != nil { 90 | t.Fatalf("Writing %d records: got %v, want nil", len(recs), err) 91 | } 92 | 93 | out := "07070100000000000041FF0000000000000000000000000000000000000000000000000000000000000000000000000000000200000000a\x0007070100000000000041FF0000000000000000000000000000000000000000000000000000000000000000000000000000000400000000a/b\x00\x00\x0007070100000000000041FF0000000000000000000000000000000000000000000000000000000000000000000000000000000600000000a/b/c\x0007070100000000000041FF0000000000000000000000000000000000000000000000000000000000000000000000000000000800000000a/b/c/d\x00\x00\x0007070100000000000041FF0000000000000000000000000000000000000000000000000000000000000000000000000000000800000000a/b/c/e\x00\x00\x00" 94 | if b.String() != out { 95 | t.Fatalf("%q != %q", b.String(), out) 96 | } 97 | if err := WriteRecordsAndDirs(w, recs); !errors.Is(err, os.ErrExist) { 98 | t.Fatalf("Writing %d records: got %v, want %v", len(recs), err, os.ErrExist) 99 | } 100 | // Test a bad write. 101 | if err := WriteRecordsAndDirs(&bad{err: fs.ErrInvalid}, recs); !errors.Is(err, fs.ErrInvalid) { 102 | t.Fatalf("Writing %d records: got %v, want %v", len(recs), err, fs.ErrInvalid) 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /cross-compile.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | GO="go" 6 | if [ -v GOROOT ]; 7 | then 8 | GO="$GOROOT/bin/go" 9 | fi 10 | 11 | function buildem() { 12 | for GOOS in $1 13 | do 14 | for GOARCH in $2 15 | do 16 | echo "Building $GOOS/$GOARCH..." 17 | GOOS=$GOOS GOARCH=$GOARCH $GO build ./... 18 | done 19 | done 20 | } 21 | 22 | GOARCHES="386 amd64 arm arm64 ppc64 ppc64le s390x mips mipsle mips64 mips64le" 23 | buildem "linux" "$GOARCHES" 24 | 25 | GOARCHES="386 amd64 arm arm64" 26 | GOOSES="freebsd windows" # TBD netbsd openbsd plan9 27 | buildem "$GOOSES" "$GOARCHES" 28 | 29 | GOARCHES_DARWIN="arm64 amd64" 30 | buildem "darwin" "$GOARCHES_DARWIN" 31 | -------------------------------------------------------------------------------- /dependencies.go: -------------------------------------------------------------------------------- 1 | //go:build tools 2 | 3 | package mkuimage 4 | 5 | // List u-root commands that need to be in go.mod & go.sum to be buildable as 6 | // dependencies. This way, they aren't eliminated by `go mod tidy`. 7 | // 8 | // But obviously aren't actually importable, since they are main packages. 9 | import ( 10 | _ "github.com/u-root/u-root/cmds/core/dhclient" 11 | _ "github.com/u-root/u-root/cmds/core/echo" 12 | _ "github.com/u-root/u-root/cmds/core/init" 13 | _ "github.com/u-root/u-root/cmds/core/ip" 14 | _ "github.com/u-root/u-root/cmds/core/ls" 15 | ) 16 | -------------------------------------------------------------------------------- /fileflag/flagfile.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 fileflag supports flag files. 6 | package fileflag 7 | 8 | import ( 9 | "fmt" 10 | "strconv" 11 | "strings" 12 | ) 13 | 14 | // ArgvToFile encodes argv program arguments such that they can be stored in a 15 | // file. 16 | func ArgvToFile(args []string) string { 17 | // We separate flags in the flags file with new lines, so we 18 | // have to escape new lines. 19 | // 20 | // Go already has a nifty Quote mechanism which will escape 21 | // more than just new-line, which won't hurt anyway. 22 | var quoted []string 23 | for _, arg := range args { 24 | quoted = append(quoted, strconv.Quote(arg)) 25 | } 26 | return strings.Join(quoted, "\n") 27 | } 28 | 29 | // FileToArgv converts argvs stored in a file back to an array of strings. 30 | func FileToArgv(content string) []string { 31 | quotedArgs := strings.Split(content, "\n") 32 | var args []string 33 | for _, arg := range quotedArgs { 34 | if len(arg) > 0 { 35 | s, err := strconv.Unquote(arg) 36 | if err != nil { 37 | panic(fmt.Sprintf("flags file encoded wrong, arg %q, error %v", arg, err)) 38 | } 39 | args = append(args, s) 40 | } 41 | } 42 | return args 43 | } 44 | -------------------------------------------------------------------------------- /fileflag/flagfile_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 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 fileflag 6 | 7 | import ( 8 | "reflect" 9 | "testing" 10 | ) 11 | 12 | func TestArgvs(t *testing.T) { 13 | for _, tt := range []struct { 14 | argv []string 15 | }{ 16 | { 17 | argv: []string{"--append=\"foobar\nfoobaz\"", "--haha"}, 18 | }, 19 | { 20 | argv: []string{"oh damn", "--append=\"foobar foobaz\"", "--haha"}, 21 | }, 22 | { 23 | argv: []string{}, 24 | }, 25 | } { 26 | got := FileToArgv(ArgvToFile(tt.argv)) 27 | // Accept nil for []string{} by checking len == 0. 28 | if !(len(tt.argv) == 0 && len(got) == 0) && !reflect.DeepEqual(got, tt.argv) { 29 | t.Errorf("FileToArgv(ArgvToFile(%#v)) = %#v, wanted original value back", tt.argv, got) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/u-root/mkuimage 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/dustin/go-humanize v1.0.1 7 | github.com/google/go-cmp v0.5.9 8 | github.com/hugelgupf/go-shlex v0.0.0-20200702092117-c80c9d0918fa 9 | github.com/u-root/gobusybox/src v0.0.0-20240226024758-7e6217d0eb49 10 | github.com/u-root/u-root v0.12.0 11 | github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a 12 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 13 | golang.org/x/sync v0.6.0 14 | golang.org/x/sys v0.17.0 15 | gopkg.in/yaml.v3 v3.0.0 16 | ) 17 | 18 | require ( 19 | github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 // indirect 20 | github.com/josharian/native v1.1.0 // indirect 21 | github.com/klauspost/compress v1.17.4 // indirect 22 | github.com/klauspost/pgzip v1.2.6 // indirect 23 | github.com/mdlayher/packet v1.1.2 // indirect 24 | github.com/mdlayher/socket v0.5.0 // indirect 25 | github.com/pierrec/lz4/v4 v4.1.14 // indirect 26 | github.com/spf13/pflag v1.0.5 // indirect 27 | github.com/ulikunitz/xz v0.5.11 // indirect 28 | github.com/vishvananda/netlink v1.2.1-beta.2 // indirect 29 | github.com/vishvananda/netns v0.0.4 // indirect 30 | golang.org/x/mod v0.15.0 // indirect 31 | golang.org/x/net v0.21.0 // indirect 32 | golang.org/x/tools v0.18.0 // indirect 33 | mvdan.cc/sh/v3 v3.7.0 // indirect 34 | ) 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= 2 | github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= 3 | github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= 4 | github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= 8 | github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= 9 | github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= 10 | github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 11 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 12 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 13 | github.com/hugelgupf/go-shlex v0.0.0-20200702092117-c80c9d0918fa h1:s3KPo0nThtvjEamF/aElD4k5jSsBHew3/sgNTnth+2M= 14 | github.com/hugelgupf/go-shlex v0.0.0-20200702092117-c80c9d0918fa/go.mod h1:I1uW6ymzwsy5TlQgD1bFAghdMgBYqH1qtCeHoZgHMqs= 15 | github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= 16 | github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= 17 | github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f h1:ov45/OzrJG8EKbGjn7jJZQJTN7Z1t73sFYNIRd64YlI= 18 | github.com/hugelgupf/vmtest v0.0.0-20240102225328-693afabdd27f/go.mod h1:JoDrYMZpDPYo6uH9/f6Peqms3zNNWT2XiGgioMOIGuI= 19 | github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2 h1:9K06NfxkBh25x56yVhWWlKFE8YpicaSfHwoV8SFbueA= 20 | github.com/insomniacslk/dhcp v0.0.0-20231206064809-8c70d406f6d2/go.mod h1:3A9PQ1cunSDF/1rbTq99Ts4pVnycWg+vlPkfeD2NLFI= 21 | github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA= 22 | github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 23 | github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= 24 | github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= 25 | github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU= 26 | github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs= 27 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 28 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 29 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 30 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 31 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 32 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 33 | github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY= 34 | github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4= 35 | github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI= 36 | github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= 37 | github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= 38 | github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= 39 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 40 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 41 | github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97 h1:3RPlVWzZ/PDqmVuf/FKHARG5EMid/tl7cv54Sw/QRVY= 42 | github.com/rogpeppe/go-internal v1.10.1-0.20230524175051-ec119421bb97/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= 43 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 44 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 45 | github.com/stretchr/objx v0.1.0 h1:4G4v2dO3VZwixGIRoQ5Lfboy6nUhCyYzaqnIAPPhYs4= 46 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 47 | github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= 48 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 49 | github.com/u-root/gobusybox/src v0.0.0-20240226024758-7e6217d0eb49 h1:Ajo25H4yhdgtbflwzhbhUu8TgMvsnTrMwdJa4KMukmo= 50 | github.com/u-root/gobusybox/src v0.0.0-20240226024758-7e6217d0eb49/go.mod h1:PW3wGFCHjdHxAhra5FKvcARbCGqGfentYuPKmuhv8DY= 51 | github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs= 52 | github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI= 53 | github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og= 54 | github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= 55 | github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= 56 | github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= 57 | github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= 58 | github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= 59 | github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= 60 | github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= 61 | github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 62 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= 63 | golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= 64 | golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8= 65 | golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 66 | golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= 67 | golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= 68 | golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= 69 | golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 70 | golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 71 | golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 72 | golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= 73 | golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 74 | golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ= 75 | golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg= 76 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 77 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 78 | gopkg.in/yaml.v3 v3.0.0 h1:hjy8E9ON/egN1tAYqKb61G10WtihqetD4sz2H+8nIeA= 79 | gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 80 | mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= 81 | mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= 82 | src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f h1:pjVeIo9Ba6K1Wy+rlwX91zT7A+xGEmxiNRBdN04gDTQ= 83 | src.elv.sh v0.16.0-rc1.0.20220116211855-fda62502ad7f/go.mod h1:kPbhv5+fBeUh85nET3wWhHGUaUQ64nZMJ8FwA5v5Olg= 84 | -------------------------------------------------------------------------------- /ldd/ldd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009-2023 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 | //go:build freebsd || linux || darwin 6 | // +build freebsd linux darwin 7 | 8 | // Package ldd returns library dependencies of an executable. 9 | // 10 | // The way this is done on GNU-based systems is interesting. For each ELF, one 11 | // finds the .interp section. If there is no interpreter there's not much to 12 | // do. 13 | // 14 | // If there is an interpreter, we run it with the --list option and the file as 15 | // an argument. We need to parse the output. For all lines with => as the 2nd 16 | // field, we take the 3rd field as a dependency. 17 | // 18 | // On many Unix kernels, the kernel ABI is stable. On OSX, the stability 19 | // is held in the library interface; the kernel ABI is explicitly not 20 | // stable. The ldd package on OSX will only return the files passed to it. 21 | package ldd 22 | -------------------------------------------------------------------------------- /ldd/ldd_other.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 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 | //go:build !linux && !freebsd 6 | // +build !linux,!freebsd 7 | 8 | package ldd 9 | 10 | // List returns nothing. 11 | // 12 | // It's not an error for a file to not be an ELF. 13 | func List(names ...string) ([]string, error) { 14 | return nil, nil 15 | } 16 | 17 | // FList returns nothing. 18 | // 19 | // It's not an error for a file to not be an ELF. 20 | func FList(names ...string) ([]string, error) { 21 | return nil, nil 22 | } 23 | -------------------------------------------------------------------------------- /ldd/ldd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017-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 | //go:build freebsd || linux 6 | // +build freebsd linux 7 | 8 | package ldd 9 | 10 | import ( 11 | "strings" 12 | "testing" 13 | ) 14 | 15 | // TestLdd tests Ldd against /bin/date. 16 | // This is just about guaranteed to have 17 | // some output on most linux systems. 18 | func TestLdd(t *testing.T) { 19 | n, err := List("/bin/date") 20 | if err != nil { 21 | t.Fatalf("Ldd on /bin/date: want nil, got %v", err) 22 | } 23 | t.Logf("TestLdd: /bin/date has deps of") 24 | for i := range n { 25 | t.Logf("\t%v", n[i]) 26 | } 27 | } 28 | 29 | // TestLddList tests that the LddList is the 30 | // same as the info returned by Ldd. 31 | func TestLddList(t *testing.T) { 32 | n, err := List("/bin/date") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | // Find the first name in the array that contains "lib" 38 | // Test 'em all 39 | for _, f := range n { 40 | if !strings.Contains(f, "lib") { 41 | continue 42 | } 43 | t.Logf("Test %v", f) 44 | n, err := List(f) 45 | if err != nil { 46 | t.Error(err) 47 | } 48 | t.Logf("%v has deps of %v", f, n) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /ldd/ldd_unix.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009-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 | //go:build freebsd || linux 6 | // +build freebsd linux 7 | 8 | package ldd 9 | 10 | import ( 11 | "debug/elf" 12 | "fmt" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "strings" 17 | ) 18 | 19 | func parseinterp(input string) ([]string, error) { 20 | var names []string 21 | for _, p := range strings.Split(input, "\n") { 22 | f := strings.Fields(p) 23 | if len(f) < 3 { 24 | continue 25 | } 26 | if f[1] != "=>" || len(f[2]) == 0 { 27 | continue 28 | } 29 | if f[0] == f[2] { 30 | continue 31 | } 32 | // If the third part is a memory address instead 33 | // of a file system path, the entry should be skipped. 34 | // For example: linux-vdso.so.1 => (0x00007ffe4972d000) 35 | if f[1] == "=>" && string(f[2][0]) == "(" { 36 | continue 37 | } 38 | names = append(names, f[2]) 39 | } 40 | return names, nil 41 | } 42 | 43 | // runinterp runs the interpreter with the --list switch 44 | // and the file as an argument. For each returned line 45 | // it looks for => as the second field, indicating a 46 | // real .so (as opposed to the .vdso or a string like 47 | // 'not a dynamic executable'. 48 | func runinterp(interp, file string) ([]string, error) { 49 | o, err := exec.Command(interp, "--list", file).Output() 50 | if err != nil { 51 | if ee, ok := err.(*exec.ExitError); ok { 52 | return nil, fmt.Errorf("%s: %s", err, ee.Stderr) 53 | } 54 | return nil, err 55 | } 56 | return parseinterp(string(o)) 57 | } 58 | 59 | // GetInterp returns the interpreter file path for the given ELF. 60 | // 61 | // It is not an error for file not to be an ELF. 62 | func GetInterp(file string) (string, error) { 63 | r, err := os.Open(file) 64 | if err != nil { 65 | return "fail", err 66 | } 67 | defer r.Close() 68 | f, err := elf.NewFile(r) 69 | if err != nil { 70 | // Not an ELF is not an error. 71 | return "", nil //nolint:nilerr 72 | } 73 | 74 | s := f.Section(".interp") 75 | var interp string 76 | if s != nil { 77 | // If there is an interpreter section, it should be 78 | // an error if we can't read it. 79 | i, err := s.Data() 80 | if err != nil { 81 | return "fail", err 82 | } 83 | 84 | // .interp section is file name + \0 character. 85 | interp := strings.TrimRight(string(i), "\000") 86 | 87 | // Ignore #! interpreters 88 | if strings.HasPrefix(interp, "#!") { 89 | return "", nil 90 | } 91 | return interp, nil 92 | } 93 | 94 | if interp == "" { 95 | if f.Type != elf.ET_DYN || f.Class == elf.ELFCLASSNONE { 96 | return "", nil 97 | } 98 | bit64 := true 99 | if f.Class != elf.ELFCLASS64 { 100 | bit64 = false 101 | } 102 | 103 | // This is a shared library. Turns out you can run an 104 | // interpreter with --list and this shared library as an 105 | // argument. What interpreter do we use? Well, there's no way to 106 | // know. You have to guess. I'm not sure why they could not 107 | // just put an interp section in .so's but maybe that would 108 | // cause trouble somewhere else. 109 | interp, err = LdSo(bit64) 110 | if err != nil { 111 | return "fail", err 112 | } 113 | } 114 | return interp, nil 115 | } 116 | 117 | // follow returns all paths and any files they recursively point to through 118 | // symlinks. 119 | func follow(paths ...string) ([]string, error) { 120 | seen := make(map[string]struct{}) 121 | 122 | for _, path := range paths { 123 | if err := followInternal(path, seen); err != nil { 124 | return nil, err 125 | } 126 | } 127 | 128 | deps := make([]string, 0, len(seen)) 129 | for s := range seen { 130 | deps = append(deps, s) 131 | } 132 | return deps, nil 133 | } 134 | 135 | func followInternal(path string, seen map[string]struct{}) error { 136 | for { 137 | if _, ok := seen[path]; ok { 138 | return nil 139 | } 140 | i, err := os.Lstat(path) 141 | if err != nil { 142 | return err 143 | } 144 | 145 | seen[path] = struct{}{} 146 | if i.Mode().IsRegular() { 147 | return nil 148 | } 149 | 150 | // If it's a symlink, read works; if not, it fails. 151 | // We can skip testing the type, since we still have to 152 | // handle any error if it's a link. 153 | next, err := os.Readlink(path) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | // A relative link has to be interpreted relative to the file's 159 | // parent's path. 160 | if !filepath.IsAbs(next) { 161 | next = filepath.Join(filepath.Dir(path), next) 162 | } 163 | path = next 164 | } 165 | } 166 | 167 | // List returns a list of all library dependencies for a set of files. 168 | // 169 | // If a file has no dependencies, that is not an error. The only possible error 170 | // is if a file does not exist, or it says it has an interpreter but we can't 171 | // read it, or we are not able to run its interpreter. 172 | // 173 | // It's not an error for a file to not be an ELF. 174 | func List(names ...string) ([]string, error) { 175 | list := make(map[string]struct{}) 176 | interps := make(map[string]struct{}) 177 | for _, n := range names { 178 | interp, err := GetInterp(n) 179 | if err != nil { 180 | return nil, err 181 | } 182 | if interp == "" { 183 | continue 184 | } 185 | interps[interp] = struct{}{} 186 | 187 | // Run the interpreter to get dependencies. 188 | sonames, err := runinterp(interp, n) 189 | if err != nil { 190 | return nil, err 191 | } 192 | for _, name := range sonames { 193 | list[name] = struct{}{} 194 | } 195 | } 196 | 197 | libs := make([]string, 0, len(list)+len(interps)) 198 | 199 | // People expect to see the interps first. 200 | for s := range interps { 201 | libs = append(libs, s) 202 | } 203 | for s := range list { 204 | libs = append(libs, s) 205 | } 206 | return libs, nil 207 | } 208 | 209 | // FList returns a list of all library dependencies for a set of files, 210 | // including following symlinks. 211 | // 212 | // If a file has no dependencies, that is not an error. The only possible error 213 | // is if a file does not exist, or it says it has an interpreter but we can't 214 | // read it, or we are not able to run its interpreter. 215 | // 216 | // It's not an error for a file to not be an ELF. 217 | func FList(names ...string) ([]string, error) { 218 | deps, err := List(names...) 219 | if err != nil { 220 | return nil, err 221 | } 222 | return follow(deps...) 223 | } 224 | -------------------------------------------------------------------------------- /ldd/ldd_unix_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2009-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 | //go:build freebsd || linux 6 | // +build freebsd linux 7 | 8 | package ldd 9 | 10 | import ( 11 | "testing" 12 | ) 13 | 14 | var ( 15 | cases = []struct { 16 | name string 17 | input string 18 | output []string 19 | }{ 20 | { 21 | name: "single vdso entry", 22 | input: ` linux-vdso.so.1`, 23 | output: []string{}, 24 | }, 25 | { 26 | name: "duplicate vdso symlink", 27 | input: ` linux-vdso.so.1 => linux-vdso.so.1`, 28 | output: []string{}, 29 | }, 30 | { 31 | name: "multiple entries", 32 | input: ` linux-vdso.so.1 => linux-vdso.so.1 33 | libc.so.6 => /usr/lib/libc.so.6 34 | /lib64/ld-linux-x86-64.so.2 => /usr/lib64/ld-linux-x86-64.so.2`, 35 | output: []string{"/usr/lib/libc.so.6", "/usr/lib64/ld-linux-x86-64.so.2"}, 36 | }, 37 | { 38 | name: "entry with memory address", 39 | input: `linux-vdso.so.1 => (0x00007ffe4972d000)`, 40 | output: []string{}, 41 | }, 42 | } 43 | ) 44 | 45 | func cmp(a, b []string) bool { 46 | if len(a) != len(b) { 47 | return false 48 | } 49 | for i, v := range a { 50 | if v != b[i] { 51 | return false 52 | } 53 | } 54 | return true 55 | } 56 | 57 | func TestParseInterp(t *testing.T) { 58 | for _, c := range cases { 59 | out, _ := parseinterp(c.input) 60 | if !cmp(out, c.output) { 61 | t.Fatalf("'%s' expected %v, but got %v", c.name, c.output, out) 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ldd/ldso_freebsd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017-2019 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 ldd 6 | 7 | import ( 8 | "fmt" 9 | "path/filepath" 10 | ) 11 | 12 | // LdSo finds the loader binary. 13 | func LdSo(bit64 bool) (string, error) { 14 | path := "/libexec/ld-elf32.so.*" 15 | if bit64 { 16 | path = "/libexec/ld-elf.so.*" 17 | } 18 | n, err := filepath.Glob(path) 19 | if err != nil { 20 | return "", err 21 | } 22 | if len(n) > 0 { 23 | return n[0], nil 24 | } 25 | return "", fmt.Errorf("could not find ld.so in %v", path) 26 | } 27 | -------------------------------------------------------------------------------- /ldd/ldso_linux.go: -------------------------------------------------------------------------------- 1 | // Copyright 2017-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 ldd 6 | 7 | import ( 8 | "fmt" 9 | "path/filepath" 10 | ) 11 | 12 | // LdSo finds the loader binary. 13 | func LdSo(bit64 bool) (string, error) { 14 | bits := 32 15 | if bit64 { 16 | bits = 64 17 | } 18 | choices := []string{fmt.Sprintf("/lib%d/ld-*.so.*", bits), "/lib/ld-*.so.*"} 19 | for _, d := range choices { 20 | n, err := filepath.Glob(d) 21 | if err != nil { 22 | return "", err 23 | } 24 | if len(n) > 0 { 25 | return n[0], nil 26 | } 27 | } 28 | return "", fmt.Errorf("could not find ld.so in %v", choices) 29 | } 30 | -------------------------------------------------------------------------------- /uimage/builder/binary.go: -------------------------------------------------------------------------------- 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 builder 6 | 7 | import ( 8 | "fmt" 9 | "path/filepath" 10 | "sync" 11 | 12 | "github.com/u-root/mkuimage/uimage/initramfs" 13 | "github.com/u-root/uio/llog" 14 | ) 15 | 16 | // BinaryBuilder builds each Go command as a separate binary. 17 | // 18 | // BinaryBuilder is an implementation of Builder. 19 | type BinaryBuilder struct{} 20 | 21 | // DefaultBinaryDir implements Builder.DefaultBinaryDir. 22 | // 23 | // "bin" is the default initramfs binary directory for these binaries. 24 | func (BinaryBuilder) DefaultBinaryDir() string { 25 | return "bin" 26 | } 27 | 28 | // Build implements Builder.Build. 29 | func (b BinaryBuilder) Build(l *llog.Logger, af *initramfs.Files, opts Opts) error { 30 | if opts.Env == nil { 31 | return ErrEnvMissing 32 | } 33 | if opts.TempDir == "" { 34 | return ErrTempDirMissing 35 | } 36 | binaryDir := opts.BinaryDir 37 | if binaryDir == "" { 38 | binaryDir = b.DefaultBinaryDir() 39 | } 40 | 41 | result := make(chan error, len(opts.Packages)) 42 | 43 | var wg sync.WaitGroup 44 | for _, pkg := range opts.Packages { 45 | wg.Add(1) 46 | go func(p string) { 47 | defer wg.Done() 48 | result <- opts.Env.Build( 49 | filepath.Join(opts.TempDir, binaryDir, filepath.Base(p)), 50 | []string{p}, 51 | opts.BuildOpts) 52 | }(pkg) 53 | } 54 | 55 | wg.Wait() 56 | close(result) 57 | 58 | for err := range result { 59 | if err != nil { 60 | return fmt.Errorf("%w: %w", ErrBinaryFailed, err) 61 | } 62 | } 63 | 64 | // Add bin directory to archive. 65 | return af.AddFile(opts.TempDir, "") 66 | } 67 | -------------------------------------------------------------------------------- /uimage/builder/binary_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2022 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 builder 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "testing" 11 | 12 | "github.com/u-root/gobusybox/src/pkg/golang" 13 | "github.com/u-root/mkuimage/uimage/initramfs" 14 | "github.com/u-root/uio/llog" 15 | ) 16 | 17 | func TestBinaryBuild(t *testing.T) { 18 | opts := Opts{ 19 | Env: golang.Default(golang.DisableCGO()), 20 | Packages: []string{ 21 | "../../cmd/mkuimage", 22 | "github.com/u-root/u-root/cmds/core/init", 23 | "cmd/test2json", 24 | }, 25 | TempDir: t.TempDir(), 26 | } 27 | af := initramfs.NewFiles() 28 | var b BinaryBuilder 29 | if err := b.Build(llog.Test(t), af, opts); err != nil { 30 | t.Fatalf("Build(%v, %v); %v != nil", af, opts, err) 31 | } 32 | 33 | mustContain := []string{ 34 | "bin/mkuimage", 35 | "bin/test2json", 36 | "bin/init", 37 | } 38 | for _, name := range mustContain { 39 | if !af.Contains(name) { 40 | t.Errorf("expected files to include %q; archive: %v", name, af) 41 | } 42 | } 43 | } 44 | 45 | func TestBinaryBuildError(t *testing.T) { 46 | for i, tt := range []struct { 47 | opts Opts 48 | want error 49 | }{ 50 | { 51 | opts: Opts{ 52 | Env: golang.Default(golang.DisableCGO()), 53 | Packages: []string{ 54 | // Does not exist. 55 | "../../cmd/foobar", 56 | }, 57 | TempDir: t.TempDir(), 58 | BinaryDir: "bbin", 59 | }, 60 | want: ErrBinaryFailed, 61 | }, 62 | { 63 | opts: Opts{ 64 | Env: golang.Default(golang.DisableCGO()), 65 | Packages: []string{ 66 | "../../cmd/mkuimage", 67 | }, 68 | BinaryDir: "bbin", 69 | }, 70 | want: ErrTempDirMissing, 71 | }, 72 | { 73 | opts: Opts{ 74 | TempDir: t.TempDir(), 75 | Packages: []string{ 76 | "../../cmd/mkuimage", 77 | }, 78 | BinaryDir: "bbin", 79 | }, 80 | want: ErrEnvMissing, 81 | }, 82 | } { 83 | t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 84 | af := initramfs.NewFiles() 85 | var b BinaryBuilder 86 | if err := b.Build(llog.Test(t), af, tt.opts); !errors.Is(err, tt.want) { 87 | t.Errorf("Build = %v, want %v", err, tt.want) 88 | } 89 | }) 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /uimage/builder/builder.go: -------------------------------------------------------------------------------- 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 builder has methods for building many Go commands into an initramfs 6 | // archive. 7 | package builder 8 | 9 | import ( 10 | "errors" 11 | 12 | "github.com/u-root/gobusybox/src/pkg/golang" 13 | "github.com/u-root/mkuimage/uimage/initramfs" 14 | "github.com/u-root/uio/llog" 15 | ) 16 | 17 | var ( 18 | // Busybox is a shared GBBBuilder instance. 19 | Busybox = &GBBBuilder{} 20 | 21 | // Binary is a shared BinaryBuilder instance. 22 | Binary = &BinaryBuilder{} 23 | ) 24 | 25 | // Possible build errors. 26 | var ( 27 | ErrBusyboxFailed = errors.New("gobusybox build failed") 28 | ErrBinaryFailed = errors.New("binary build failed") 29 | ErrEnvMissing = errors.New("must specify Go build environment") 30 | ErrTempDirMissing = errors.New("must supply temporary directory for build") 31 | ) 32 | 33 | // Opts are options passed to the Builder.Build function. 34 | type Opts struct { 35 | // Env is the Go compiler environment. 36 | Env *golang.Environ 37 | 38 | // Build options for building go binaries. Ultimate this holds all the 39 | // args that end up being passed to `go build`. 40 | BuildOpts *golang.BuildOpts 41 | 42 | // Packages are the Go packages to compile. 43 | // 44 | // Only an explicit list of absolute directory paths is accepted. 45 | Packages []string 46 | 47 | // TempDir is a temporary directory where the compilation mode compiled 48 | // binaries can be placed. 49 | // 50 | // TempDir should contain no files. 51 | TempDir string 52 | 53 | // BinaryDir is the initramfs directory for built binaries. 54 | // 55 | // BinaryDir must be specified. 56 | BinaryDir string 57 | } 58 | 59 | // Builder builds Go packages and adds the binaries to an initramfs. 60 | // 61 | // The resulting files need not be binaries per se, but exec'ing the resulting 62 | // file should result in the Go program being executed. 63 | type Builder interface { 64 | // Build uses the given options to build Go packages and adds its files 65 | // to be included in the initramfs to the given ArchiveFiles. 66 | Build(*llog.Logger, *initramfs.Files, Opts) error 67 | 68 | // DefaultBinaryDir is the initramfs' default directory for binaries 69 | // built using this builder. 70 | DefaultBinaryDir() string 71 | } 72 | -------------------------------------------------------------------------------- /uimage/builder/gbb.go: -------------------------------------------------------------------------------- 1 | // Copyright 2015-2021 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 builder 6 | 7 | import ( 8 | "fmt" 9 | "log/slog" 10 | "path" 11 | "path/filepath" 12 | 13 | "github.com/u-root/gobusybox/src/pkg/bb" 14 | "github.com/u-root/mkuimage/cpio" 15 | "github.com/u-root/mkuimage/uimage/initramfs" 16 | "github.com/u-root/uio/llog" 17 | ) 18 | 19 | // Commands to skip building in bb mode. 20 | var skip = map[string]struct{}{ 21 | "bb": {}, 22 | } 23 | 24 | // GBBBuilder is an implementation of Builder that compiles many Go commands 25 | // into one busybox-style binary. 26 | // 27 | // GBBBuilder will also include symlinks for each command to the busybox binary. 28 | // 29 | // GBBBuilder does all this by rewriting the source files of the packages given 30 | // to create one busybox-like binary containing all commands. 31 | // 32 | // The compiled binary uses argv[0] to decide which Go command to run. 33 | // 34 | // See bb/README.md for a detailed explanation of the implementation of busybox 35 | // mode. 36 | type GBBBuilder struct { 37 | // ShellBang means generate #! files instead of symlinks. 38 | // ShellBang are more portable and just as efficient. 39 | ShellBang bool 40 | } 41 | 42 | // DefaultBinaryDir implements Builder.DefaultBinaryDir. 43 | // 44 | // The default initramfs binary dir is bbin for busybox binaries. 45 | func (GBBBuilder) DefaultBinaryDir() string { 46 | return "bbin" 47 | } 48 | 49 | // Build is an implementation of Builder.Build for a busybox-like initramfs. 50 | func (b *GBBBuilder) Build(l *llog.Logger, af *initramfs.Files, opts Opts) error { 51 | // Build the busybox binary. 52 | if len(opts.TempDir) == 0 { 53 | return ErrTempDirMissing 54 | } 55 | if opts.Env == nil { 56 | return ErrEnvMissing 57 | } 58 | bbPath := filepath.Join(opts.TempDir, "bb") 59 | 60 | binaryDir := opts.BinaryDir 61 | if binaryDir == "" { 62 | binaryDir = b.DefaultBinaryDir() 63 | } 64 | 65 | bopts := &bb.Opts{ 66 | Env: opts.Env, 67 | GenSrcDir: opts.TempDir, 68 | CommandPaths: opts.Packages, 69 | BinaryPath: bbPath, 70 | GoBuildOpts: opts.BuildOpts, 71 | } 72 | if err := bb.BuildBusybox(l.AtLevel(slog.LevelInfo), bopts); err != nil { 73 | return fmt.Errorf("%w: %w", ErrBusyboxFailed, err) 74 | } 75 | 76 | if err := af.AddFile(bbPath, "bbin/bb"); err != nil { 77 | return err 78 | } 79 | 80 | // Add symlinks for included commands to initramfs. 81 | for _, pkg := range opts.Packages { 82 | if _, ok := skip[path.Base(pkg)]; ok { 83 | continue 84 | } 85 | 86 | // Add a symlink /bbin/{cmd} -> /bbin/bb to our initramfs. 87 | // Or add a #! file if b.ShellBang is set ... 88 | if b.ShellBang { 89 | b := path.Base(pkg) 90 | if err := af.AddRecord(cpio.StaticFile(filepath.Join(binaryDir, b), "#!/bbin/bb #!"+b+"\n", 0o755)); err != nil { 91 | return err 92 | } 93 | } else if err := af.AddRecord(cpio.Symlink(filepath.Join(binaryDir, path.Base(pkg)), "bb")); err != nil { 94 | return err 95 | } 96 | } 97 | return nil 98 | } 99 | -------------------------------------------------------------------------------- /uimage/builder/gbb_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 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 builder 6 | 7 | import ( 8 | "errors" 9 | "os" 10 | "testing" 11 | 12 | "github.com/u-root/gobusybox/src/pkg/golang" 13 | "github.com/u-root/mkuimage/cpio" 14 | "github.com/u-root/mkuimage/uimage/initramfs" 15 | "github.com/u-root/uio/llog" 16 | ) 17 | 18 | func TestGBBBuild(t *testing.T) { 19 | dir := t.TempDir() 20 | 21 | opts := Opts{ 22 | Env: golang.Default(golang.DisableCGO()), 23 | Packages: []string{ 24 | "../../cmd/mkuimage", 25 | }, 26 | TempDir: dir, 27 | } 28 | af := initramfs.NewFiles() 29 | var gbb GBBBuilder 30 | if err := gbb.Build(llog.Test(t), af, opts); err != nil { 31 | t.Fatalf("Build(%v, %v); %v != nil", af, opts, err) 32 | } 33 | 34 | mustContain := []string{ 35 | "bbin/mkuimage", 36 | "bbin/bb", 37 | } 38 | for _, name := range mustContain { 39 | if !af.Contains(name) { 40 | t.Errorf("expected files to include %q; archive: %v", name, af) 41 | } 42 | } 43 | } 44 | 45 | func TestGBBBuildError(t *testing.T) { 46 | for _, tt := range []struct { 47 | gbb GBBBuilder 48 | files []cpio.Record 49 | opts Opts 50 | want error 51 | }{ 52 | { 53 | opts: Opts{ 54 | Env: golang.Default(golang.DisableCGO()), 55 | Packages: []string{ 56 | "../../cmd/mkuimage", 57 | }, 58 | BinaryDir: "bbin", 59 | }, 60 | want: ErrTempDirMissing, 61 | }, 62 | { 63 | opts: Opts{ 64 | TempDir: t.TempDir(), 65 | Packages: []string{ 66 | "../../cmd/mkuimage", 67 | }, 68 | BinaryDir: "bbin", 69 | }, 70 | want: ErrEnvMissing, 71 | }, 72 | { 73 | opts: Opts{ 74 | Env: golang.Default(golang.DisableCGO()), 75 | TempDir: t.TempDir(), 76 | Packages: []string{ 77 | "../../cmd/mkuimage", 78 | }, 79 | BinaryDir: "bbin", 80 | }, 81 | files: []cpio.Record{ 82 | cpio.StaticFile("bbin/bb", "", 0o777), 83 | }, 84 | want: os.ErrExist, 85 | }, 86 | { 87 | opts: Opts{ 88 | Env: golang.Default(golang.DisableCGO()), 89 | TempDir: t.TempDir(), 90 | Packages: []string{ 91 | "../../cmd/mkuimage", 92 | }, 93 | BinaryDir: "bbin", 94 | }, 95 | files: []cpio.Record{ 96 | cpio.StaticFile("bbin/mkuimage", "", 0o777), 97 | }, 98 | want: os.ErrExist, 99 | }, 100 | { 101 | opts: Opts{ 102 | Env: golang.Default(golang.DisableCGO()), 103 | TempDir: t.TempDir(), 104 | Packages: []string{ 105 | "../../cmd/mkuimage", 106 | }, 107 | BinaryDir: "bbin", 108 | }, 109 | files: []cpio.Record{ 110 | cpio.StaticFile("bbin/mkuimage", "", 0o777), 111 | }, 112 | gbb: GBBBuilder{ShellBang: true}, 113 | want: os.ErrExist, 114 | }, 115 | } { 116 | af := initramfs.NewFiles() 117 | for _, f := range tt.files { 118 | _ = af.AddRecord(f) 119 | } 120 | if err := tt.gbb.Build(llog.Test(t), af, tt.opts); !errors.Is(err, tt.want) { 121 | t.Errorf("Build = %v, want %v", err, tt.want) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /uimage/initramfs/archive.go: -------------------------------------------------------------------------------- 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 initramfs can write archives out to CPIO or directories. 6 | package initramfs 7 | 8 | import ( 9 | "errors" 10 | "io" 11 | 12 | "github.com/u-root/mkuimage/cpio" 13 | ) 14 | 15 | // Possible errors. 16 | var ( 17 | ErrNoPath = errors.New("invalid argument: must specify path") 18 | ) 19 | 20 | // ReadOpener opens a cpio.RecordReader. 21 | type ReadOpener interface { 22 | OpenReader() (cpio.RecordReader, error) 23 | } 24 | 25 | // WriteOpener opens a Writer. 26 | type WriteOpener interface { 27 | OpenWriter() (Writer, error) 28 | } 29 | 30 | // Writer is an initramfs archive that files can be written to. 31 | type Writer interface { 32 | cpio.RecordWriter 33 | 34 | // Finish finishes the archive. 35 | Finish() error 36 | } 37 | 38 | // Opts are options for building an initramfs archive. 39 | type Opts struct { 40 | // Files are the files to be included. 41 | // 42 | // Files here generally have priority over files in DefaultRecords or 43 | // BaseArchive. 44 | *Files 45 | 46 | // OutputFile is the file to write to. 47 | OutputFile WriteOpener 48 | 49 | // BaseArchive is an existing archive to add files to. 50 | // 51 | // BaseArchive may be nil. 52 | BaseArchive ReadOpener 53 | 54 | // UseExistingInit determines whether the init from BaseArchive is used 55 | // or not, if BaseArchive is specified. 56 | // 57 | // If this is false, the "init" file in BaseArchive will be renamed 58 | // "inito" (for init-original) in the output archive. 59 | UseExistingInit bool 60 | } 61 | 62 | // Write uses the given options to determine which files to write to the output 63 | // initramfs. 64 | func Write(opts *Opts) error { 65 | // Write base archive. 66 | if opts.BaseArchive != nil { 67 | base, err := opts.BaseArchive.OpenReader() 68 | if err != nil { 69 | return err 70 | } 71 | transform := cpio.MakeReproducible 72 | 73 | // Rename init to inito if user doesn't want the existing init. 74 | if !opts.UseExistingInit && opts.Contains("init") { 75 | transform = func(r cpio.Record) cpio.Record { 76 | if r.Name == "init" { 77 | r.Name = "inito" 78 | } 79 | return cpio.MakeReproducible(r) 80 | } 81 | } 82 | // If user wants the base archive init, but specified another 83 | // init, make the other one inito. 84 | if opts.UseExistingInit && opts.Contains("init") { 85 | opts.Rename("init", "inito") 86 | } 87 | 88 | for { 89 | f, err := base.ReadRecord() 90 | if err == io.EOF { 91 | break 92 | } 93 | if err != nil { 94 | return err 95 | } 96 | // TODO: ignore only the error where it already exists 97 | // in archive. 98 | _ = opts.Files.AddRecord(transform(f)) 99 | } 100 | } 101 | 102 | out, err := opts.OutputFile.OpenWriter() 103 | if err != nil { 104 | return err 105 | } 106 | if err := opts.Files.WriteTo(out); err != nil { 107 | return err 108 | } 109 | return out.Finish() 110 | } 111 | -------------------------------------------------------------------------------- /uimage/initramfs/cpio.go: -------------------------------------------------------------------------------- 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 initramfs 6 | 7 | import ( 8 | "fmt" 9 | "os" 10 | 11 | "github.com/u-root/mkuimage/cpio" 12 | ) 13 | 14 | // CPIOFile opens a Reader or Writer that reads/writes files from/to a CPIO archive at the given path. 15 | type CPIOFile struct { 16 | Path string 17 | } 18 | 19 | var _ ReadOpener = &CPIOFile{} 20 | var _ WriteOpener = &CPIOFile{} 21 | 22 | // OpenWriter opens c.Path for writing. 23 | func (c *CPIOFile) OpenWriter() (Writer, error) { 24 | if len(c.Path) == 0 { 25 | return nil, fmt.Errorf("failed to write to CPIO: %w", ErrNoPath) 26 | } 27 | f, err := os.OpenFile(c.Path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return cpioWriter{cpio.Newc.Writer(f), f}, nil 32 | } 33 | 34 | // OpenReader opens c.Path for reading. 35 | func (c *CPIOFile) OpenReader() (cpio.RecordReader, error) { 36 | if len(c.Path) == 0 { 37 | return nil, fmt.Errorf("failed to read from CPIO: %w", ErrNoPath) 38 | } 39 | f, err := os.Open(c.Path) 40 | if err != nil { 41 | return nil, err 42 | } 43 | return cpio.Newc.Reader(f), nil 44 | } 45 | 46 | // Archive opens a Reader that reads files from an in-memory archive. 47 | type Archive struct { 48 | *cpio.Archive 49 | } 50 | 51 | var _ ReadOpener = &Archive{} 52 | 53 | // OpenWriter writes to the archive. 54 | func (a *Archive) OpenWriter() (Writer, error) { 55 | return cpioWriter{a.Archive, nil}, nil 56 | } 57 | 58 | // OpenReader opens the archive for reading. 59 | func (a *Archive) OpenReader() (cpio.RecordReader, error) { 60 | return a.Archive.Reader(), nil 61 | } 62 | 63 | // osWriter implements Writer. 64 | type cpioWriter struct { 65 | cpio.RecordWriter 66 | 67 | f *os.File 68 | } 69 | 70 | // Finish implements Writer.Finish. 71 | func (o cpioWriter) Finish() error { 72 | err := cpio.WriteTrailer(o) 73 | if o.f != nil { 74 | o.f.Close() 75 | } 76 | return err 77 | } 78 | -------------------------------------------------------------------------------- /uimage/initramfs/dir.go: -------------------------------------------------------------------------------- 1 | // Copyright 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 initramfs 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "os" 11 | 12 | "github.com/u-root/mkuimage/cpio" 13 | ) 14 | 15 | // Dir opens a Writer that writes all archive files to the given directory. 16 | type Dir struct { 17 | Path string 18 | } 19 | 20 | var _ WriteOpener = &Dir{} 21 | 22 | // OpenWriter implements Archiver.OpenWriter. 23 | func (d *Dir) OpenWriter() (Writer, error) { 24 | if len(d.Path) == 0 { 25 | return nil, fmt.Errorf("failed to use directory as output: %w", ErrNoPath) 26 | } 27 | if err := os.MkdirAll(d.Path, 0o755); err != nil && !errors.Is(err, os.ErrExist) { 28 | return nil, err 29 | } 30 | return dirWriter{d.Path}, nil 31 | } 32 | 33 | // dirWriter implements Writer. 34 | type dirWriter struct { 35 | dir string 36 | } 37 | 38 | // WriteRecord implements Writer.WriteRecord. 39 | func (dw dirWriter) WriteRecord(r cpio.Record) error { 40 | return cpio.CreateFileInRoot(r, dw.dir, false) 41 | } 42 | 43 | // Finish implements Writer.Finish. 44 | func (dw dirWriter) Finish() error { 45 | return nil 46 | } 47 | -------------------------------------------------------------------------------- /uimage/initramfs/files.go: -------------------------------------------------------------------------------- 1 | // Copyright 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 initramfs 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "log" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "sort" 15 | 16 | "github.com/u-root/mkuimage/cpio" 17 | ) 18 | 19 | // Files are host files and records to add to the resulting initramfs. 20 | type Files struct { 21 | // Files is a map of relative archive path -> absolute host file path. 22 | Files map[string]string 23 | 24 | // Records is a map of relative archive path -> Record to use. 25 | // 26 | // TODO: While the only archive mode is cpio, this will be a 27 | // cpio.Record. If or when there is another archival mode, we can add a 28 | // similar uroot.Record type. 29 | Records map[string]cpio.Record 30 | } 31 | 32 | // NewFiles returns a new archive files map. 33 | func NewFiles() *Files { 34 | return &Files{ 35 | Files: make(map[string]string), 36 | Records: make(map[string]cpio.Record), 37 | } 38 | } 39 | 40 | // sortedKeys returns a list of sorted paths in the archive. 41 | func (af *Files) sortedKeys() []string { 42 | keys := make([]string, 0, len(af.Files)+len(af.Records)) 43 | for dest := range af.Files { 44 | keys = append(keys, dest) 45 | } 46 | for dest := range af.Records { 47 | keys = append(keys, dest) 48 | } 49 | sort.Strings(keys) 50 | return keys 51 | } 52 | 53 | func (af *Files) addFile(src string, dest string, follow bool) error { 54 | src = filepath.Clean(src) 55 | dest = path.Clean(dest) 56 | if path.IsAbs(dest) { 57 | r, err := filepath.Rel("/", dest) 58 | if err != nil { 59 | return fmt.Errorf("%q is an absolute path and can't make it relative to /: %v", dest, err) 60 | } 61 | log.Printf("Warning: You used an absolute path %q and it was adjusted to %q", dest, r) 62 | dest = r 63 | } 64 | 65 | if follow { 66 | s, err := filepath.EvalSymlinks(src) 67 | if err != nil { 68 | return err 69 | } 70 | src = s 71 | } 72 | 73 | // We check if it's a directory first. If a directory already exists as 74 | // a record or file, we want to include its children anyway. 75 | sInfo, err := os.Lstat(src) 76 | if err != nil { 77 | return fmt.Errorf("adding %q to archive failed because Lstat failed: %w", src, err) 78 | } 79 | 80 | // Recursively add children. 81 | if sInfo.Mode().IsDir() { 82 | err := children(src, func(name string) error { 83 | return af.addFile(filepath.Join(src, name), filepath.Join(dest, name), follow) 84 | }) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | // Only override an existing directory if all children were 90 | // added successfully. 91 | af.Files[dest] = src 92 | return nil 93 | } 94 | 95 | if record, ok := af.Records[dest]; ok { 96 | return &os.PathError{ 97 | Op: "add to archive", 98 | Path: dest, 99 | Err: fmt.Errorf("%w: is %v", os.ErrExist, record), 100 | } 101 | } 102 | 103 | if srcFile, ok := af.Files[dest]; ok { 104 | // Just a duplicate. 105 | if src == srcFile { 106 | return nil 107 | } 108 | return &os.PathError{ 109 | Op: "add to archive", 110 | Path: dest, 111 | Err: fmt.Errorf("%w: backed by %q", os.ErrExist, src), 112 | } 113 | } 114 | 115 | af.Files[dest] = src 116 | return nil 117 | } 118 | 119 | // AddFile adds a host file at src into the archive at dest. 120 | // It follows symlinks. 121 | // 122 | // If src is a directory, it and its children will be added to the archive 123 | // relative to dest. 124 | // 125 | // Duplicate files with identical content will be silently ignored. 126 | func (af *Files) AddFile(src string, dest string) error { 127 | return af.addFile(src, dest, true) 128 | } 129 | 130 | // AddFileNoFollow adds a host file at src into the archive at dest. 131 | // It does not follow symlinks. 132 | // 133 | // If src is a directory, it and its children will be added to the archive 134 | // relative to dest. 135 | // 136 | // Duplicate files with identical content will be silently ignored. 137 | func (af *Files) AddFileNoFollow(src string, dest string) error { 138 | return af.addFile(src, dest, false) 139 | } 140 | 141 | var errAbsoluteName = errors.New("record name must not be absolute") 142 | 143 | // AddRecord adds a cpio.Record into the archive at `r.Name`. 144 | func (af *Files) AddRecord(r cpio.Record) error { 145 | r.Name = path.Clean(r.Name) 146 | if filepath.IsAbs(r.Name) { 147 | return fmt.Errorf("%w: %q", errAbsoluteName, r.Name) 148 | } 149 | 150 | if src, ok := af.Files[r.Name]; ok { 151 | return &os.PathError{ 152 | Op: "add to archive", 153 | Path: r.Name, 154 | Err: fmt.Errorf("%w: backed by %q", os.ErrExist, src), 155 | } 156 | } 157 | if record, ok := af.Records[r.Name]; ok { 158 | if record.Info == r.Info { 159 | return nil 160 | } 161 | return &os.PathError{ 162 | Op: "add to archive", 163 | Path: r.Name, 164 | Err: fmt.Errorf("%w: is %v", os.ErrExist, record), 165 | } 166 | } 167 | 168 | af.Records[r.Name] = r 169 | return nil 170 | } 171 | 172 | // Contains returns whether path `dest` is already contained in the archive. 173 | func (af *Files) Contains(dest string) bool { 174 | _, fok := af.Files[dest] 175 | _, rok := af.Records[dest] 176 | return fok || rok 177 | } 178 | 179 | // Rename renames a file in the archive. 180 | func (af *Files) Rename(name string, newname string) { 181 | if src, ok := af.Files[name]; ok { 182 | delete(af.Files, name) 183 | af.Files[newname] = src 184 | } 185 | if record, ok := af.Records[name]; ok { 186 | delete(af.Records, name) 187 | record.Name = newname 188 | af.Records[newname] = record 189 | } 190 | } 191 | 192 | // addParent recursively adds parent directory records for `name`. 193 | func (af *Files) addParent(name string) { 194 | parent := path.Dir(name) 195 | if parent == "." { 196 | return 197 | } 198 | if !af.Contains(parent) { 199 | _ = af.AddRecord(cpio.Directory(parent, 0o755)) 200 | } 201 | af.addParent(parent) 202 | } 203 | 204 | // fillInParents adds parent directory records for unparented files in `af`. 205 | func (af *Files) fillInParents() { 206 | for name := range af.Files { 207 | af.addParent(name) 208 | } 209 | for name := range af.Records { 210 | af.addParent(name) 211 | } 212 | } 213 | 214 | // WriteTo writes all records and files in `af` to `w`. 215 | func (af *Files) WriteTo(w Writer) error { 216 | // Add parent directories when not added specifically. 217 | af.fillInParents() 218 | cr := cpio.NewRecorder() 219 | 220 | // Reproducible builds: Files should be added to the archive in the 221 | // same order. 222 | for _, path := range af.sortedKeys() { 223 | if record, ok := af.Records[path]; ok { 224 | if err := w.WriteRecord(record); err != nil { 225 | return err 226 | } 227 | } 228 | if src, ok := af.Files[path]; ok { 229 | if err := writeFile(w, cr, src, path); err != nil { 230 | return err 231 | } 232 | } 233 | } 234 | return nil 235 | } 236 | 237 | // writeFile takes the file at `src` on the host system and adds it to the 238 | // archive `w` at path `dest`. 239 | // 240 | // If `src` is a directory, its children will be added to the archive as well. 241 | func writeFile(w Writer, r *cpio.Recorder, src, dest string) error { 242 | record, err := r.GetRecord(src) 243 | if err != nil { 244 | return err 245 | } 246 | 247 | // Fix the name. 248 | record.Name = dest 249 | return w.WriteRecord(cpio.MakeReproducible(record)) 250 | } 251 | 252 | // children calls `fn` on all direct children of directory `dir`. 253 | func children(dir string, fn func(name string) error) error { 254 | f, err := os.Open(dir) 255 | if err != nil { 256 | return err 257 | } 258 | names, err := f.Readdirnames(-1) 259 | f.Close() 260 | if err != nil { 261 | return err 262 | } 263 | 264 | for _, name := range names { 265 | if err := fn(name); os.IsNotExist(err) { 266 | // File was deleted in the meantime. 267 | continue 268 | } else if err != nil { 269 | return err 270 | } 271 | } 272 | return nil 273 | } 274 | -------------------------------------------------------------------------------- /uimage/initramfs/files_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 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 initramfs 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "os" 11 | "path/filepath" 12 | "reflect" 13 | "testing" 14 | 15 | "github.com/u-root/mkuimage/cpio" 16 | "github.com/u-root/uio/uio" 17 | ) 18 | 19 | func archive(tb testing.TB, rs ...cpio.Record) *cpio.Archive { 20 | tb.Helper() 21 | a, err := cpio.ArchiveFromRecords(rs) 22 | if err != nil { 23 | tb.Fatal(err) 24 | } 25 | return a 26 | } 27 | 28 | func TestFilesAddFileNoFollow(t *testing.T) { 29 | regularFile, err := os.CreateTemp("", "archive-files-add-file") 30 | if err != nil { 31 | t.Error(err) 32 | } 33 | defer os.RemoveAll(regularFile.Name()) 34 | 35 | dir := t.TempDir() 36 | dir2 := t.TempDir() 37 | 38 | //nolint:errcheck 39 | { 40 | os.Create(filepath.Join(dir, "foo2")) 41 | os.Symlink(filepath.Join(dir, "foo2"), filepath.Join(dir2, "foo3")) 42 | } 43 | 44 | for i, tt := range []struct { 45 | name string 46 | af *Files 47 | src string 48 | dest string 49 | result *Files 50 | err error 51 | }{ 52 | { 53 | name: "just add a file", 54 | af: NewFiles(), 55 | 56 | src: regularFile.Name(), 57 | dest: "bar/foo", 58 | 59 | result: &Files{ 60 | Files: map[string]string{ 61 | "bar/foo": regularFile.Name(), 62 | }, 63 | Records: map[string]cpio.Record{}, 64 | }, 65 | }, 66 | { 67 | name: "add symlinked file, NOT following", 68 | af: NewFiles(), 69 | src: filepath.Join(dir2, "foo3"), 70 | dest: "bar/foo", 71 | result: &Files{ 72 | Files: map[string]string{ 73 | "bar/foo": filepath.Join(dir2, "foo3"), 74 | }, 75 | Records: map[string]cpio.Record{}, 76 | }, 77 | }, 78 | } { 79 | t.Run(fmt.Sprintf("Test %02d: %s", i, tt.name), func(t *testing.T) { 80 | err := tt.af.AddFileNoFollow(tt.src, tt.dest) 81 | if !errors.Is(err, tt.err) { 82 | t.Errorf("AddFileNoFollow = %v, want %v", err, tt.err) 83 | } 84 | 85 | if tt.result != nil && !reflect.DeepEqual(tt.af, tt.result) { 86 | t.Errorf("got %v, want %v", tt.af, tt.result) 87 | } 88 | }) 89 | } 90 | } 91 | 92 | func TestFilesAddFile(t *testing.T) { 93 | regularFile, err := os.CreateTemp("", "archive-files-add-file") 94 | if err != nil { 95 | t.Error(err) 96 | } 97 | defer os.RemoveAll(regularFile.Name()) 98 | 99 | dir := t.TempDir() 100 | dir2 := t.TempDir() 101 | dir3 := t.TempDir() 102 | 103 | symlinkToDir3 := filepath.Join(dir3, "fooSymDir/") 104 | fooDir := filepath.Join(dir3, "fooDir") 105 | _ = os.WriteFile(filepath.Join(dir, "foo"), nil, 0o777) 106 | _ = os.WriteFile(filepath.Join(dir, "foo2"), nil, 0o777) 107 | _ = os.Symlink(filepath.Join(dir, "foo2"), filepath.Join(dir2, "foo3")) 108 | 109 | _ = os.Mkdir(fooDir, os.ModePerm) 110 | _ = os.Symlink(fooDir, symlinkToDir3) 111 | _ = os.WriteFile(filepath.Join(fooDir, "foo"), nil, 0o777) 112 | _ = os.WriteFile(filepath.Join(fooDir, "bar"), nil, 0o777) 113 | 114 | for i, tt := range []struct { 115 | name string 116 | af *Files 117 | src string 118 | dest string 119 | result *Files 120 | err error 121 | }{ 122 | { 123 | name: "just add a file", 124 | af: NewFiles(), 125 | 126 | src: regularFile.Name(), 127 | dest: "bar/foo", 128 | 129 | result: &Files{ 130 | Files: map[string]string{ 131 | "bar/foo": regularFile.Name(), 132 | }, 133 | Records: map[string]cpio.Record{}, 134 | }, 135 | }, 136 | { 137 | name: "add symlinked file, following", 138 | af: NewFiles(), 139 | src: filepath.Join(dir2, "foo3"), 140 | dest: "bar/foo", 141 | result: &Files{ 142 | Files: map[string]string{ 143 | "bar/foo": filepath.Join(dir, "foo2"), 144 | }, 145 | Records: map[string]cpio.Record{}, 146 | }, 147 | }, 148 | { 149 | name: "add symlinked directory, following", 150 | af: NewFiles(), 151 | src: symlinkToDir3, 152 | dest: "foo/", 153 | result: &Files{ 154 | Files: map[string]string{ 155 | "foo": fooDir, 156 | "foo/foo": filepath.Join(fooDir, "foo"), 157 | "foo/bar": filepath.Join(fooDir, "bar"), 158 | }, 159 | Records: map[string]cpio.Record{}, 160 | }, 161 | }, 162 | { 163 | name: "add file that exists in Files", 164 | af: &Files{ 165 | Files: map[string]string{ 166 | "bar/foo": "/some/other/place", 167 | }, 168 | }, 169 | src: regularFile.Name(), 170 | dest: "bar/foo", 171 | result: &Files{ 172 | Files: map[string]string{ 173 | "bar/foo": "/some/other/place", 174 | }, 175 | }, 176 | err: os.ErrExist, 177 | }, 178 | { 179 | name: "add a file that exists in Records", 180 | af: &Files{ 181 | Records: map[string]cpio.Record{ 182 | "bar/foo": cpio.Symlink("bar/foo", "/some/other/place"), 183 | }, 184 | }, 185 | src: regularFile.Name(), 186 | dest: "bar/foo", 187 | result: &Files{ 188 | Records: map[string]cpio.Record{ 189 | "bar/foo": cpio.Symlink("bar/foo", "/some/other/place"), 190 | }, 191 | }, 192 | err: os.ErrExist, 193 | }, 194 | { 195 | name: "add a file that already exists in Files, but is the same one", 196 | af: &Files{ 197 | Files: map[string]string{ 198 | "bar/foo": regularFile.Name(), 199 | }, 200 | }, 201 | src: regularFile.Name(), 202 | dest: "bar/foo", 203 | result: &Files{ 204 | Files: map[string]string{ 205 | "bar/foo": regularFile.Name(), 206 | }, 207 | }, 208 | }, 209 | { 210 | name: "absolute destination paths are made relative", 211 | af: &Files{ 212 | Files: map[string]string{}, 213 | }, 214 | src: dir, 215 | dest: "/bar/foo", 216 | result: &Files{ 217 | Files: map[string]string{ 218 | "bar/foo": dir, 219 | "bar/foo/foo": filepath.Join(dir, "foo"), 220 | "bar/foo/foo2": filepath.Join(dir, "foo2"), 221 | }, 222 | }, 223 | }, 224 | { 225 | name: "add a directory", 226 | af: &Files{ 227 | Files: map[string]string{}, 228 | }, 229 | src: dir, 230 | dest: "bar/foo", 231 | result: &Files{ 232 | Files: map[string]string{ 233 | "bar/foo": dir, 234 | "bar/foo/foo": filepath.Join(dir, "foo"), 235 | "bar/foo/foo2": filepath.Join(dir, "foo2"), 236 | }, 237 | }, 238 | }, 239 | { 240 | name: "add a different directory to the same destination, no overlapping children", 241 | af: &Files{ 242 | Files: map[string]string{ 243 | "bar/foo": "/some/place/real", 244 | "bar/foo/zed": "/some/place/real/zed", 245 | }, 246 | }, 247 | src: dir, 248 | dest: "bar/foo", 249 | result: &Files{ 250 | Files: map[string]string{ 251 | "bar/foo": dir, 252 | "bar/foo/foo": filepath.Join(dir, "foo"), 253 | "bar/foo/foo2": filepath.Join(dir, "foo2"), 254 | "bar/foo/zed": "/some/place/real/zed", 255 | }, 256 | }, 257 | }, 258 | { 259 | name: "add a different directory to the same destination, overlapping children", 260 | af: &Files{ 261 | Files: map[string]string{ 262 | "bar/foo": "/some/place/real", 263 | "bar/foo/foo2": "/some/place/real/zed", 264 | }, 265 | }, 266 | src: dir, 267 | dest: "bar/foo", 268 | err: os.ErrExist, 269 | }, 270 | } { 271 | t.Run(fmt.Sprintf("Test %02d: %s", i, tt.name), func(t *testing.T) { 272 | err := tt.af.AddFile(tt.src, tt.dest) 273 | if !errors.Is(err, tt.err) { 274 | t.Errorf("AddFile = %v, want %v", err, tt.err) 275 | } 276 | 277 | if tt.result != nil && !reflect.DeepEqual(tt.af, tt.result) { 278 | t.Errorf("got %v, want %v", tt.af, tt.result) 279 | } 280 | }) 281 | } 282 | } 283 | 284 | func TestFilesAddRecord(t *testing.T) { 285 | for i, tt := range []struct { 286 | af *Files 287 | record cpio.Record 288 | 289 | result *Files 290 | err error 291 | }{ 292 | { 293 | af: NewFiles(), 294 | record: cpio.Symlink("bar/foo", ""), 295 | result: &Files{ 296 | Files: map[string]string{}, 297 | Records: map[string]cpio.Record{ 298 | "bar/foo": cpio.Symlink("bar/foo", ""), 299 | }, 300 | }, 301 | }, 302 | { 303 | af: &Files{ 304 | Files: map[string]string{ 305 | "bar/foo": "/some/other/place", 306 | }, 307 | }, 308 | record: cpio.Symlink("bar/foo", ""), 309 | result: &Files{ 310 | Files: map[string]string{ 311 | "bar/foo": "/some/other/place", 312 | }, 313 | }, 314 | err: os.ErrExist, 315 | }, 316 | { 317 | af: &Files{ 318 | Records: map[string]cpio.Record{ 319 | "bar/foo": cpio.Symlink("bar/foo", "/some/other/place"), 320 | }, 321 | }, 322 | record: cpio.Symlink("bar/foo", ""), 323 | result: &Files{ 324 | Records: map[string]cpio.Record{ 325 | "bar/foo": cpio.Symlink("bar/foo", "/some/other/place"), 326 | }, 327 | }, 328 | err: os.ErrExist, 329 | }, 330 | { 331 | af: &Files{ 332 | Records: map[string]cpio.Record{ 333 | "bar/foo": cpio.Symlink("bar/foo", "/some/other/place"), 334 | }, 335 | }, 336 | record: cpio.Symlink("bar/foo", "/some/other/place"), 337 | result: &Files{ 338 | Records: map[string]cpio.Record{ 339 | "bar/foo": cpio.Symlink("bar/foo", "/some/other/place"), 340 | }, 341 | }, 342 | }, 343 | { 344 | record: cpio.Symlink("/bar/foo", ""), 345 | err: errAbsoluteName, 346 | }, 347 | } { 348 | t.Run(fmt.Sprintf("Test %02d", i), func(t *testing.T) { 349 | err := tt.af.AddRecord(tt.record) 350 | if !errors.Is(err, tt.err) { 351 | t.Errorf("AddRecord = %v, want %v", err, tt.err) 352 | } 353 | 354 | if !reflect.DeepEqual(tt.af, tt.result) { 355 | t.Errorf("got %v, want %v", tt.af, tt.result) 356 | } 357 | }) 358 | } 359 | } 360 | 361 | func TestFilesfillInParent(t *testing.T) { 362 | for i, tt := range []struct { 363 | af *Files 364 | result *Files 365 | }{ 366 | { 367 | af: &Files{ 368 | Records: map[string]cpio.Record{ 369 | "foo/bar": cpio.Directory("foo/bar", 0o777), 370 | }, 371 | }, 372 | result: &Files{ 373 | Records: map[string]cpio.Record{ 374 | "foo/bar": cpio.Directory("foo/bar", 0o777), 375 | "foo": cpio.Directory("foo", 0o755), 376 | }, 377 | }, 378 | }, 379 | { 380 | af: &Files{ 381 | Files: map[string]string{ 382 | "baz/baz/baz": "/somewhere", 383 | }, 384 | Records: map[string]cpio.Record{ 385 | "foo/bar": cpio.Directory("foo/bar", 0o777), 386 | }, 387 | }, 388 | result: &Files{ 389 | Files: map[string]string{ 390 | "baz/baz/baz": "/somewhere", 391 | }, 392 | Records: map[string]cpio.Record{ 393 | "foo/bar": cpio.Directory("foo/bar", 0o777), 394 | "foo": cpio.Directory("foo", 0o755), 395 | "baz": cpio.Directory("baz", 0o755), 396 | "baz/baz": cpio.Directory("baz/baz", 0o755), 397 | }, 398 | }, 399 | }, 400 | { 401 | af: &Files{}, 402 | result: &Files{}, 403 | }, 404 | } { 405 | t.Run(fmt.Sprintf("Test %02d", i), func(t *testing.T) { 406 | tt.af.fillInParents() 407 | if !reflect.DeepEqual(tt.af, tt.result) { 408 | t.Errorf("got %v, want %v", tt.af, tt.result) 409 | } 410 | }) 411 | } 412 | } 413 | 414 | type Records map[string]cpio.Record 415 | 416 | func recordsEqual(r1, r2 Records, recordEqual func(cpio.Record, cpio.Record) bool) bool { 417 | for name, s1 := range r1 { 418 | s2, ok := r2[name] 419 | if !ok { 420 | return false 421 | } 422 | if !recordEqual(s1, s2) { 423 | return false 424 | } 425 | } 426 | for name := range r2 { 427 | if _, ok := r1[name]; !ok { 428 | return false 429 | } 430 | } 431 | return true 432 | } 433 | 434 | func sameNameModeContent(r1 cpio.Record, r2 cpio.Record) bool { 435 | if r1.Name != r2.Name || r1.Mode != r2.Mode { 436 | return false 437 | } 438 | return uio.ReaderAtEqual(r1.ReaderAt, r2.ReaderAt) 439 | } 440 | 441 | func TestOptsWrite(t *testing.T) { 442 | for i, tt := range []struct { 443 | desc string 444 | opts *Opts 445 | output *cpio.Archive 446 | want Records 447 | err error 448 | }{ 449 | { 450 | desc: "no conflicts, just records", 451 | opts: &Opts{ 452 | Files: &Files{ 453 | Records: map[string]cpio.Record{ 454 | "foo": cpio.Symlink("foo", "elsewhere"), 455 | }, 456 | }, 457 | BaseArchive: &Archive{Archive: archive(t, 458 | cpio.Directory("etc", 0o777), 459 | cpio.Directory("etc/nginx", 0o777), 460 | )}, 461 | }, 462 | output: &cpio.Archive{ 463 | Files: make(map[string]cpio.Record), 464 | }, 465 | want: Records{ 466 | "foo": cpio.Symlink("foo", "elsewhere"), 467 | "etc": cpio.Directory("etc", 0o777), 468 | "etc/nginx": cpio.Directory("etc/nginx", 0o777), 469 | cpio.Trailer: cpio.TrailerRecord, 470 | }, 471 | }, 472 | { 473 | desc: "default already exists", 474 | opts: &Opts{ 475 | Files: &Files{ 476 | Records: map[string]cpio.Record{ 477 | "etc": cpio.Symlink("etc", "whatever"), 478 | }, 479 | }, 480 | BaseArchive: &Archive{Archive: archive(t, 481 | cpio.Directory("etc", 0o777), 482 | )}, 483 | }, 484 | output: &cpio.Archive{ 485 | Files: make(map[string]cpio.Record), 486 | }, 487 | want: Records{ 488 | "etc": cpio.Symlink("etc", "whatever"), 489 | cpio.Trailer: cpio.TrailerRecord, 490 | }, 491 | }, 492 | { 493 | desc: "no conflicts, missing parent automatically created", 494 | opts: &Opts{ 495 | Files: &Files{ 496 | Records: map[string]cpio.Record{ 497 | "foo/bar/baz": cpio.Symlink("foo/bar/baz", "elsewhere"), 498 | }, 499 | }, 500 | BaseArchive: nil, 501 | }, 502 | output: &cpio.Archive{ 503 | Files: make(map[string]cpio.Record), 504 | }, 505 | want: Records{ 506 | "foo": cpio.Directory("foo", 0o755), 507 | "foo/bar": cpio.Directory("foo/bar", 0o755), 508 | "foo/bar/baz": cpio.Symlink("foo/bar/baz", "elsewhere"), 509 | cpio.Trailer: cpio.TrailerRecord, 510 | }, 511 | }, 512 | { 513 | desc: "parent only automatically created if not already exists", 514 | opts: &Opts{ 515 | Files: &Files{ 516 | Records: map[string]cpio.Record{ 517 | "foo/bar": cpio.Directory("foo/bar", 0o444), 518 | "foo/bar/baz": cpio.Symlink("foo/bar/baz", "elsewhere"), 519 | }, 520 | }, 521 | BaseArchive: nil, 522 | }, 523 | output: &cpio.Archive{ 524 | Files: make(map[string]cpio.Record), 525 | }, 526 | want: Records{ 527 | "foo": cpio.Directory("foo", 0o755), 528 | "foo/bar": cpio.Directory("foo/bar", 0o444), 529 | "foo/bar/baz": cpio.Symlink("foo/bar/baz", "elsewhere"), 530 | cpio.Trailer: cpio.TrailerRecord, 531 | }, 532 | }, 533 | { 534 | desc: "base archive", 535 | opts: &Opts{ 536 | Files: &Files{ 537 | Records: map[string]cpio.Record{ 538 | "foo/bar": cpio.Symlink("foo/bar", "elsewhere"), 539 | "exists": cpio.Directory("exists", 0o777), 540 | }, 541 | }, 542 | BaseArchive: &Archive{Archive: archive(t, 543 | cpio.Directory("etc", 0o755), 544 | cpio.Directory("foo", 0o444), 545 | cpio.Directory("exists", 0), 546 | )}, 547 | }, 548 | output: &cpio.Archive{ 549 | Files: make(map[string]cpio.Record), 550 | }, 551 | want: Records{ 552 | "etc": cpio.Directory("etc", 0o755), 553 | "exists": cpio.Directory("exists", 0o777), 554 | "foo": cpio.Directory("foo", 0o444), 555 | "foo/bar": cpio.Symlink("foo/bar", "elsewhere"), 556 | cpio.Trailer: cpio.TrailerRecord, 557 | }, 558 | }, 559 | { 560 | desc: "base archive with init, no user init", 561 | opts: &Opts{ 562 | Files: &Files{ 563 | Records: map[string]cpio.Record{}, 564 | }, 565 | BaseArchive: &Archive{Archive: archive(t, 566 | cpio.StaticFile("init", "boo", 0o555), 567 | )}, 568 | }, 569 | output: &cpio.Archive{ 570 | Files: make(map[string]cpio.Record), 571 | }, 572 | want: Records{ 573 | "init": cpio.StaticFile("init", "boo", 0o555), 574 | cpio.Trailer: cpio.TrailerRecord, 575 | }, 576 | }, 577 | { 578 | desc: "base archive with init and user init", 579 | opts: &Opts{ 580 | Files: &Files{ 581 | Records: map[string]cpio.Record{ 582 | "init": cpio.StaticFile("init", "bar", 0o444), 583 | }, 584 | }, 585 | BaseArchive: &Archive{Archive: archive(t, 586 | cpio.StaticFile("init", "boo", 0o555), 587 | )}, 588 | }, 589 | output: &cpio.Archive{ 590 | Files: make(map[string]cpio.Record), 591 | }, 592 | want: Records{ 593 | "init": cpio.StaticFile("init", "bar", 0o444), 594 | "inito": cpio.StaticFile("inito", "boo", 0o555), 595 | cpio.Trailer: cpio.TrailerRecord, 596 | }, 597 | }, 598 | { 599 | desc: "base archive with init, use existing init", 600 | opts: &Opts{ 601 | Files: &Files{ 602 | Records: map[string]cpio.Record{}, 603 | }, 604 | BaseArchive: &Archive{Archive: archive(t, 605 | cpio.StaticFile("init", "boo", 0o555), 606 | )}, 607 | UseExistingInit: true, 608 | }, 609 | output: &cpio.Archive{ 610 | Files: make(map[string]cpio.Record), 611 | }, 612 | want: Records{ 613 | "init": cpio.StaticFile("init", "boo", 0o555), 614 | cpio.Trailer: cpio.TrailerRecord, 615 | }, 616 | }, 617 | { 618 | desc: "base archive with init and user init, use existing init", 619 | opts: &Opts{ 620 | Files: &Files{ 621 | Records: map[string]cpio.Record{ 622 | "init": cpio.StaticFile("init", "huh", 0o111), 623 | }, 624 | }, 625 | BaseArchive: &Archive{Archive: archive(t, 626 | cpio.StaticFile("init", "boo", 0o555), 627 | )}, 628 | UseExistingInit: true, 629 | }, 630 | output: &cpio.Archive{ 631 | Files: make(map[string]cpio.Record), 632 | }, 633 | want: Records{ 634 | "init": cpio.StaticFile("init", "boo", 0o555), 635 | "inito": cpio.StaticFile("inito", "huh", 0o111), 636 | cpio.Trailer: cpio.TrailerRecord, 637 | }, 638 | }, 639 | } { 640 | t.Run(fmt.Sprintf("Test %02d (%s)", i, tt.desc), func(t *testing.T) { 641 | tt.opts.OutputFile = &Archive{tt.output} 642 | 643 | if err := Write(tt.opts); !errors.Is(err, tt.err) { 644 | t.Errorf("Write = %v, want %v", err, tt.err) 645 | } 646 | 647 | if !recordsEqual(tt.output.Files, tt.want, sameNameModeContent) { 648 | t.Errorf("Write() = %v, want %v", tt.output.Files, tt.want) 649 | } 650 | }) 651 | } 652 | } 653 | -------------------------------------------------------------------------------- /uimage/initramfs/test/ramfs.go: -------------------------------------------------------------------------------- 1 | // Copyright 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 | // nolint 6 | package test 7 | 8 | import ( 9 | "fmt" 10 | "os" 11 | 12 | "github.com/u-root/mkuimage/cpio" 13 | "github.com/u-root/uio/uio" 14 | ) 15 | 16 | type ArchiveValidator interface { 17 | Validate(a *cpio.Archive) error 18 | } 19 | 20 | type HasRecord struct { 21 | R cpio.Record 22 | } 23 | 24 | func (hr HasRecord) Validate(a *cpio.Archive) error { 25 | r, ok := a.Get(hr.R.Name) 26 | if !ok { 27 | return fmt.Errorf("archive does not contain %v", hr.R) 28 | } 29 | if !cpio.Equal(r, hr.R) { 30 | return fmt.Errorf("archive does not contain %v; instead has %v", hr.R, r) 31 | } 32 | return nil 33 | } 34 | 35 | type HasFile struct { 36 | Path string 37 | } 38 | 39 | func (hf HasFile) Validate(a *cpio.Archive) error { 40 | if _, ok := a.Get(hf.Path); ok { 41 | return nil 42 | } 43 | return fmt.Errorf("archive does not contain %s, but should", hf.Path) 44 | } 45 | 46 | type HasDir struct { 47 | Path string 48 | } 49 | 50 | func (h HasDir) Validate(a *cpio.Archive) error { 51 | r, ok := a.Get(h.Path) 52 | if !ok { 53 | return fmt.Errorf("archive does not contain %s, but should", h.Path) 54 | } 55 | if r.Mode&cpio.S_IFDIR == 0 { 56 | return fmt.Errorf("file %v should be directory, but isn't", h.Path) 57 | } 58 | return nil 59 | } 60 | 61 | type HasContent struct { 62 | Path string 63 | Content string 64 | } 65 | 66 | func (hc HasContent) Validate(a *cpio.Archive) error { 67 | r, ok := a.Get(hc.Path) 68 | if !ok { 69 | return fmt.Errorf("archive does not contain %s, but should", hc.Path) 70 | } 71 | if c, err := uio.ReadAll(r); err != nil { 72 | return fmt.Errorf("reading record %s failed: %v", hc.Path, err) 73 | } else if string(c) != hc.Content { 74 | return fmt.Errorf("content of %s is %s, want %s", hc.Path, string(c), hc.Content) 75 | } 76 | return nil 77 | } 78 | 79 | type MissingFile struct { 80 | Path string 81 | } 82 | 83 | func (mf MissingFile) Validate(a *cpio.Archive) error { 84 | if _, ok := a.Get(mf.Path); ok { 85 | return fmt.Errorf("archive contains %s, but shouldn't", mf.Path) 86 | } 87 | return nil 88 | } 89 | 90 | type IsEmpty struct{} 91 | 92 | func (IsEmpty) Validate(a *cpio.Archive) error { 93 | if empty := a.Empty(); !empty { 94 | return fmt.Errorf("expected archive to be empty") 95 | } 96 | return nil 97 | } 98 | 99 | func ReadArchive(path string) (*cpio.Archive, error) { 100 | f, err := os.Open(path) 101 | if err != nil { 102 | return nil, err 103 | } 104 | 105 | return cpio.ArchiveFromReader(cpio.Newc.Reader(f)) 106 | } 107 | -------------------------------------------------------------------------------- /uimage/mkuimage/cmd.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 mkuimage 6 | 7 | import ( 8 | "bytes" 9 | "errors" 10 | "fmt" 11 | "os" 12 | "os/exec" 13 | "runtime" 14 | "strings" 15 | 16 | "github.com/u-root/gobusybox/src/pkg/golang" 17 | "github.com/u-root/mkuimage/uimage" 18 | "github.com/u-root/mkuimage/uimage/builder" 19 | "github.com/u-root/mkuimage/uimage/templates" 20 | "github.com/u-root/uio/llog" 21 | ) 22 | 23 | var recommendedVersions = []string{ 24 | "go1.20", 25 | "go1.21", 26 | "go1.22", 27 | } 28 | 29 | func isRecommendedVersion(v string) bool { 30 | for _, r := range recommendedVersions { 31 | if strings.HasPrefix(v, r) { 32 | return true 33 | } 34 | } 35 | return false 36 | } 37 | 38 | func uimageOpts(l *llog.Logger, m []uimage.Modifier, tpl *templates.Templates, f *Flags, conf string, cmds []string) (*uimage.Opts, error) { 39 | // Evaluate template first -- template settings may always be 40 | // appended/overridden by further flag-based settings. 41 | if conf != "" { 42 | mods, err := tpl.Uimage(conf) 43 | if err != nil { 44 | return nil, err 45 | } 46 | l.Debugf("Config: %#v", tpl.Configs[conf]) 47 | m = append(m, mods...) 48 | } 49 | 50 | // Expand command templates. 51 | if tpl != nil { 52 | cmds = tpl.CommandsFor(cmds...) 53 | } 54 | 55 | more, err := f.Modifiers(cmds...) 56 | if err != nil { 57 | return nil, err 58 | } 59 | return uimage.OptionsFor(append(m, more...)...) 60 | } 61 | 62 | func checkAmd64Level(l *llog.Logger, env *golang.Environ) { 63 | if env.GOARCH != "amd64" { 64 | return 65 | } 66 | 67 | // Looking for "amd64.v2" in "env.ToolTags" is unreliable; see 68 | // . Invoke "go env" instead. 69 | var bad string 70 | abiLevel, err := exec.Command("go", "env", "GOAMD64").Output() 71 | if err == nil { 72 | if bytes.Equal(abiLevel, []byte("v1\n")) { 73 | return 74 | } 75 | bad = "is not" 76 | } else { 77 | if exerr, isExErr := err.(*exec.ExitError); isExErr { 78 | l.Warnf("\"go env\" failed: %s", exerr.Stderr) 79 | } else { 80 | l.Warnf("couldn't execute \"go env\": %s", err) 81 | } 82 | bad = "may not be" 83 | } 84 | l.Warnf("GOAMD64 %s set to v1; on older CPUs, binaries built into " + 85 | "the initrd may crash or refuse to run.", bad) 86 | } 87 | 88 | // CreateUimage creates a uimage with the given base modifiers and flags, using args as the list of commands. 89 | func CreateUimage(l *llog.Logger, base []uimage.Modifier, tf *TemplateFlags, f *Flags, args []string) error { 90 | tpl, err := tf.Get() 91 | if err != nil { 92 | return fmt.Errorf("failed to get template: %w", err) 93 | } 94 | 95 | keepTempDir := f.KeepTempDir 96 | if f.TempDir == nil { 97 | tempDir, err := os.MkdirTemp("", "u-root") 98 | if err != nil { 99 | return err 100 | } 101 | f.TempDir = &tempDir 102 | defer func() { 103 | if keepTempDir { 104 | l.Infof("Keeping temp dir %s", tempDir) 105 | } else { 106 | os.RemoveAll(tempDir) 107 | } 108 | }() 109 | } else if _, err := os.Stat(*f.TempDir); os.IsNotExist(err) { 110 | if err := os.MkdirAll(*f.TempDir, 0o755); err != nil { 111 | return fmt.Errorf("temporary directory %q did not exist; tried to mkdir but failed: %v", *f.TempDir, err) 112 | } 113 | } 114 | 115 | opts, err := uimageOpts(l, base, tpl, f, tf.Config, args) 116 | if err != nil { 117 | return err 118 | } 119 | 120 | env := opts.Env 121 | 122 | l.Infof("Build environment: %s", env) 123 | if env.GOOS != "linux" { 124 | l.Warnf("GOOS is not linux. Did you mean to set GOOS=linux?") 125 | } 126 | 127 | checkAmd64Level(l, env); 128 | 129 | v, err := env.Version() 130 | if err != nil { 131 | l.Infof("Could not get environment's Go version, using runtime's version: %v", err) 132 | v = runtime.Version() 133 | } 134 | if !isRecommendedVersion(v) { 135 | l.Warnf(`You are not using one of the recommended Go versions (have = %s, recommended = %v). 136 | Some packages may not compile. 137 | Go to https://golang.org/doc/install to find out how to install a newer version of Go, 138 | or use https://godoc.org/golang.org/dl/%s to install an additional version of Go.`, 139 | v, recommendedVersions, recommendedVersions[0]) 140 | } 141 | 142 | err = opts.Create(l) 143 | if errors.Is(err, builder.ErrBusyboxFailed) { 144 | l.Errorf("Preserving temp dir due to busybox build error") 145 | keepTempDir = true 146 | } 147 | return err 148 | } 149 | -------------------------------------------------------------------------------- /uimage/mkuimage/cmd_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 mkuimage 6 | 7 | import ( 8 | "errors" 9 | "testing" 10 | 11 | "github.com/google/go-cmp/cmp" 12 | "github.com/google/go-cmp/cmp/cmpopts" 13 | "github.com/u-root/gobusybox/src/pkg/golang" 14 | "github.com/u-root/mkuimage/uimage" 15 | "github.com/u-root/mkuimage/uimage/builder" 16 | "github.com/u-root/mkuimage/uimage/initramfs" 17 | "github.com/u-root/mkuimage/uimage/templates" 18 | "github.com/u-root/uio/llog" 19 | ) 20 | 21 | func TestOpts(t *testing.T) { 22 | for _, tt := range []struct { 23 | name string 24 | m []uimage.Modifier 25 | tpl *templates.Templates 26 | f *Flags 27 | conf string 28 | cmds []string 29 | opts *uimage.Opts 30 | err error 31 | }{ 32 | { 33 | name: "cmdline only", 34 | m: []uimage.Modifier{ 35 | uimage.WithReplaceEnv(golang.Default(golang.DisableCGO())), 36 | uimage.WithCPIOOutput("/tmp/initramfs.cpio"), 37 | uimage.WithTempDir("foo"), 38 | }, 39 | f: &Flags{ 40 | Commands: CommandFlags{Builder: "bb"}, 41 | ArchiveFormat: "cpio", 42 | Init: String("init"), 43 | Uinit: String("gosh script.sh"), 44 | OutputFile: "./foo.cpio", 45 | Files: []string{"/bin/bash"}, 46 | }, 47 | cmds: []string{ 48 | "github.com/u-root/u-root/cmds/core/init", 49 | "github.com/u-root/u-root/cmds/core/gosh", 50 | }, 51 | opts: &uimage.Opts{ 52 | Env: golang.Default(golang.DisableCGO()), 53 | InitCmd: "init", 54 | UinitCmd: "gosh", 55 | UinitArgs: []string{"script.sh"}, 56 | OutputFile: &initramfs.CPIOFile{Path: "./foo.cpio"}, 57 | ExtraFiles: []string{"/bin/bash"}, 58 | TempDir: "foo", 59 | Commands: []uimage.Commands{ 60 | { 61 | Builder: &builder.GBBBuilder{}, 62 | Packages: []string{ 63 | "github.com/u-root/u-root/cmds/core/init", 64 | "github.com/u-root/u-root/cmds/core/gosh", 65 | }, 66 | }, 67 | }, 68 | }, 69 | }, 70 | { 71 | name: "template and cmdline combo", 72 | m: []uimage.Modifier{ 73 | uimage.WithReplaceEnv(golang.Default(golang.DisableCGO())), 74 | uimage.WithCPIOOutput("/tmp/initramfs.cpio"), 75 | uimage.WithTempDir("foo"), 76 | }, 77 | tpl: &templates.Templates{ 78 | Configs: map[string]templates.Config{ 79 | "plan9": templates.Config{ 80 | GOOS: "plan9", 81 | GOARCH: "amd64", 82 | BuildTags: []string{"grpcnotrace"}, 83 | Uinit: String("gosh script.sh"), 84 | Files: []string{"foobar"}, 85 | Commands: []templates.Command{ 86 | { 87 | Builder: "bb", 88 | Commands: []string{ 89 | "github.com/u-root/u-root/cmds/core/gosh", 90 | }, 91 | }, 92 | { 93 | Builder: "binary", 94 | Commands: []string{ 95 | "cmd/test2json", 96 | }, 97 | }, 98 | }, 99 | }, 100 | }, 101 | }, 102 | conf: "plan9", 103 | f: &Flags{ 104 | Commands: CommandFlags{Builder: "bb"}, 105 | ArchiveFormat: "cpio", 106 | Init: String("init"), 107 | Uinit: String("cat"), 108 | OutputFile: "./foo.cpio", 109 | Files: []string{"/bin/bash"}, 110 | }, 111 | cmds: []string{ 112 | "github.com/u-root/u-root/cmds/core/init", 113 | "github.com/u-root/u-root/cmds/core/cat", 114 | }, 115 | opts: &uimage.Opts{ 116 | Env: golang.Default(golang.DisableCGO(), golang.WithGOOS("plan9"), golang.WithGOARCH("amd64"), golang.WithBuildTag("grpcnotrace")), 117 | InitCmd: "init", 118 | UinitCmd: "cat", 119 | OutputFile: &initramfs.CPIOFile{Path: "./foo.cpio"}, 120 | ExtraFiles: []string{"foobar", "/bin/bash"}, 121 | TempDir: "foo", 122 | Commands: []uimage.Commands{ 123 | { 124 | Builder: &builder.GBBBuilder{}, 125 | Packages: []string{ 126 | "github.com/u-root/u-root/cmds/core/gosh", 127 | "github.com/u-root/u-root/cmds/core/init", 128 | "github.com/u-root/u-root/cmds/core/cat", 129 | }, 130 | }, 131 | { 132 | Builder: builder.Binary, 133 | Packages: []string{ 134 | "cmd/test2json", 135 | }, 136 | }, 137 | }, 138 | }, 139 | }, 140 | { 141 | name: "expand cmdline config", 142 | m: []uimage.Modifier{ 143 | uimage.WithReplaceEnv(golang.Default(golang.DisableCGO())), 144 | uimage.WithCPIOOutput("/tmp/initramfs.cpio"), 145 | uimage.WithTempDir("foo"), 146 | }, 147 | f: &Flags{ 148 | Commands: CommandFlags{Builder: "bb"}, 149 | ArchiveFormat: "cpio", 150 | OutputFile: "./foo.cpio", 151 | Files: []string{"/bin/bash"}, 152 | }, 153 | tpl: &templates.Templates{ 154 | Commands: map[string][]string{ 155 | "core": []string{ 156 | "github.com/u-root/u-root/cmds/core/init", 157 | "github.com/u-root/u-root/cmds/core/gosh", 158 | }, 159 | }, 160 | }, 161 | cmds: []string{"core", "github.com/u-root/u-root/cmds/core/cat"}, 162 | opts: &uimage.Opts{ 163 | Env: golang.Default(golang.DisableCGO()), 164 | OutputFile: &initramfs.CPIOFile{Path: "./foo.cpio"}, 165 | ExtraFiles: []string{"/bin/bash"}, 166 | TempDir: "foo", 167 | Commands: []uimage.Commands{ 168 | { 169 | Builder: &builder.GBBBuilder{}, 170 | Packages: []string{ 171 | "github.com/u-root/u-root/cmds/core/init", 172 | "github.com/u-root/u-root/cmds/core/gosh", 173 | "github.com/u-root/u-root/cmds/core/cat", 174 | }, 175 | }, 176 | }, 177 | }, 178 | }, 179 | } { 180 | t.Run(tt.name, func(t *testing.T) { 181 | opts, err := uimageOpts(llog.Test(t), tt.m, tt.tpl, tt.f, tt.conf, tt.cmds) 182 | if !errors.Is(err, tt.err) { 183 | t.Errorf("opts = %v, want %v", err, tt.err) 184 | } 185 | if diff := cmp.Diff(opts, tt.opts, cmpopts.IgnoreFields(uimage.Opts{}, "BaseArchive")); diff != "" { 186 | t.Errorf("opts (-got, +want) = %v", diff) 187 | } 188 | }) 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /uimage/mkuimage/uflags.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 mkuimage defines mkuimage flags and creation function. 6 | package mkuimage 7 | 8 | import ( 9 | "flag" 10 | "fmt" 11 | "os" 12 | 13 | "github.com/u-root/gobusybox/src/pkg/golang" 14 | "github.com/u-root/gobusybox/src/pkg/uflag" 15 | "github.com/u-root/mkuimage/uimage" 16 | "github.com/u-root/mkuimage/uimage/builder" 17 | "github.com/u-root/mkuimage/uimage/templates" 18 | ) 19 | 20 | type optionalStringVar struct { 21 | s **string 22 | } 23 | 24 | // Set implements flag.Value.Set. 25 | func (o optionalStringVar) Set(s string) error { 26 | *o.s = &s 27 | return nil 28 | } 29 | 30 | func (o *optionalStringVar) String() string { 31 | if o == nil || o.s == nil || *(o.s) == nil { 32 | return "" 33 | } 34 | return **(o.s) 35 | } 36 | 37 | // CommandFlags are flags related to Go commands to be built by mkuimage. 38 | type CommandFlags struct { 39 | NoCommands bool 40 | Builder string 41 | ShellBang bool 42 | Mod golang.ModBehavior 43 | BuildTags []string 44 | BuildOpts *golang.BuildOpts 45 | } 46 | 47 | // RegisterFlags registers flags related to Go commands being built. 48 | func (c *CommandFlags) RegisterFlags(f *flag.FlagSet) { 49 | f.StringVar(&c.Builder, "build", c.Builder, "uimage command build format (e.g. bb/gbb or binary).") 50 | f.BoolVar(&c.NoCommands, "nocmd", c.NoCommands, "Build no Go commands; initramfs only") 51 | f.BoolVar(&c.ShellBang, "shellbang", c.ShellBang, "Use #! instead of symlinks for busybox") 52 | if c.BuildOpts == nil { 53 | c.BuildOpts = &golang.BuildOpts{} 54 | } 55 | c.BuildOpts.RegisterFlags(f) 56 | // Register an alias for -go-no-strip for backwards compatibility. 57 | f.BoolVar(&c.BuildOpts.NoStrip, "no-strip", false, "Build unstripped binaries") 58 | 59 | // Flags for golang.Environ. 60 | f.StringVar((*string)(&c.Mod), "go-mod", string(c.Mod), "Value of -mod to go commands (allowed: (empty), vendor, mod, readonly)") 61 | // Register an alias for -go-build-tags for backwards compatibility. 62 | f.Var((*uflag.Strings)(&c.BuildTags), "tags", "Go build tags -- repeat the flag for multiple values") 63 | f.Var((*uflag.Strings)(&c.BuildTags), "go-build-tags", "Go build tags -- repeat the flag for multiple values") 64 | } 65 | 66 | // Modifiers turns the flag values into uimage modifiers. 67 | func (c *CommandFlags) Modifiers(packages ...string) ([]uimage.Modifier, error) { 68 | if c.NoCommands { 69 | // Later modifiers may still add packages, so let's set the right environment. 70 | return []uimage.Modifier{ 71 | uimage.WithEnv(golang.WithBuildTag(c.BuildTags...), func(e *golang.Environ) { 72 | e.Mod = c.Mod 73 | }), 74 | }, nil 75 | } 76 | 77 | switch c.Builder { 78 | case "bb", "gbb": 79 | return []uimage.Modifier{ 80 | uimage.WithEnv(golang.WithBuildTag(c.BuildTags...), func(e *golang.Environ) { 81 | e.Mod = c.Mod 82 | }), 83 | uimage.WithBusyboxCommands(packages...), 84 | uimage.WithShellBang(c.ShellBang), 85 | uimage.WithBusyboxBuildOpts(c.BuildOpts), 86 | }, nil 87 | case "binary": 88 | return []uimage.Modifier{ 89 | uimage.WithEnv(golang.WithBuildTag(c.BuildTags...), func(e *golang.Environ) { 90 | e.Mod = c.Mod 91 | }), 92 | uimage.WithCommands(c.BuildOpts, builder.Binary, packages...), 93 | }, nil 94 | default: 95 | return nil, fmt.Errorf("%w: could not find binary builder format %q", os.ErrInvalid, c.Builder) 96 | } 97 | } 98 | 99 | // String can be used to fill in values for Init, Uinit, and Shell. 100 | func String(s string) *string { 101 | return &s 102 | } 103 | 104 | // Flags are mkuimage command-line flags. 105 | type Flags struct { 106 | TempDir *string 107 | KeepTempDir bool 108 | 109 | Init *string 110 | Uinit *string 111 | Shell *string 112 | 113 | Files []string 114 | 115 | BaseArchive string 116 | ArchiveFormat string 117 | OutputFile string 118 | UseExistingInit bool 119 | 120 | Commands CommandFlags 121 | } 122 | 123 | // Modifiers return uimage modifiers created from the flags. 124 | func (f *Flags) Modifiers(packages ...string) ([]uimage.Modifier, error) { 125 | m := []uimage.Modifier{ 126 | uimage.WithFiles(f.Files...), 127 | uimage.WithExistingInit(f.UseExistingInit), 128 | } 129 | if f.TempDir != nil { 130 | m = append(m, uimage.WithTempDir(*f.TempDir)) 131 | } 132 | if f.BaseArchive != "" { 133 | // ArchiveFormat does not determine this, as only CPIO is supported. 134 | m = append(m, uimage.WithBaseFile(f.BaseArchive)) 135 | } 136 | if f.Init != nil { 137 | m = append(m, uimage.WithInit(*f.Init)) 138 | } 139 | if f.Uinit != nil { 140 | m = append(m, uimage.WithUinitCommand(*f.Uinit)) 141 | } 142 | if f.Shell != nil { 143 | m = append(m, uimage.WithShell(*f.Shell)) 144 | } 145 | switch f.ArchiveFormat { 146 | case "cpio": 147 | m = append(m, uimage.WithCPIOOutput(f.OutputFile)) 148 | case "dir": 149 | m = append(m, uimage.WithOutputDir(f.OutputFile)) 150 | default: 151 | return nil, fmt.Errorf("%w: could not find output format %q", os.ErrInvalid, f.ArchiveFormat) 152 | } 153 | more, err := f.Commands.Modifiers(packages...) 154 | if err != nil { 155 | return nil, err 156 | } 157 | return append(m, more...), nil 158 | } 159 | 160 | // RegisterFlags registers flags. 161 | func (f *Flags) RegisterFlags(fs *flag.FlagSet) { 162 | fs.Var(&optionalStringVar{&f.TempDir}, "tmp-dir", "Temporary directory to build binary and archive in. Deleted after build if --keep-tmp-dir is not set.") 163 | fs.BoolVar(&f.KeepTempDir, "keep-tmp-dir", f.KeepTempDir, "Keep temporary directory after build") 164 | 165 | fs.Var(&optionalStringVar{&f.Init}, "initcmd", "Symlink target for /init. Can be an absolute path or a Go command name. Use initcmd=\"\" if you don't want the symlink.") 166 | fs.Var(&optionalStringVar{&f.Uinit}, "uinitcmd", "Symlink target and arguments for /bin/uinit. Can be an absolute path or a Go command name, followed by command-line args. Use uinitcmd=\"\" if you don't want the symlink. E.g. -uinitcmd=\"echo foobar\"") 167 | fs.Var(&optionalStringVar{&f.Shell}, "defaultsh", "Default shell. Can be an absolute path or a Go command name. Use defaultsh=\"\" if you don't want the symlink.") 168 | 169 | fs.Var((*uflag.Strings)(&f.Files), "files", "Additional files, directories, and binaries (with their ldd dependencies) to add to archive. Can be specified multiple times.") 170 | 171 | fs.StringVar(&f.BaseArchive, "base", f.BaseArchive, "Base archive to add files to. By default, this is a couple of directories like /bin, /etc, etc. Has a default internally supplied set of files; use base=/dev/null if you don't want any base files.") 172 | fs.StringVar(&f.ArchiveFormat, "format", f.ArchiveFormat, "Archival input (for -base) and output (for -o) format.") 173 | fs.StringVar(&f.OutputFile, "o", f.OutputFile, "Path to output initramfs file.") 174 | fs.BoolVar(&f.UseExistingInit, "useinit", f.UseExistingInit, "Use existing init from base archive (only if --base was specified).") 175 | fs.BoolVar(&f.UseExistingInit, "use-init", f.UseExistingInit, "Use existing init from base archive (only if --base was specified).") 176 | 177 | f.Commands.RegisterFlags(fs) 178 | } 179 | 180 | // TemplateFlags are flags for uimage config templates. 181 | type TemplateFlags struct { 182 | File string 183 | Config string 184 | } 185 | 186 | // RegisterFlags registers template flags. 187 | func (tc *TemplateFlags) RegisterFlags(f *flag.FlagSet) { 188 | f.StringVar(&tc.Config, "config", "", "Config to pick from templates") 189 | f.StringVar(&tc.File, "config-file", "", "Config file to read from (default: finds .mkuimage.yaml in cwd or parents)") 190 | } 191 | 192 | // Get turns template flags into templates. 193 | func (tc *TemplateFlags) Get() (*templates.Templates, error) { 194 | if tc.File != "" { 195 | return templates.TemplateFromFile(tc.File) 196 | } 197 | 198 | tpl, err := templates.Template() 199 | // Only complain about not finding a template if user requested a templated config. 200 | if err != nil && tc.Config != "" { 201 | return nil, err 202 | } 203 | return tpl, nil 204 | } 205 | -------------------------------------------------------------------------------- /uimage/mkuimage/uflags_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 mkuimage 6 | 7 | import ( 8 | "errors" 9 | "flag" 10 | "os" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/google/go-cmp/cmp" 15 | "github.com/google/go-cmp/cmp/cmpopts" 16 | "github.com/u-root/gobusybox/src/pkg/golang" 17 | "github.com/u-root/mkuimage/uimage" 18 | "github.com/u-root/mkuimage/uimage/builder" 19 | ) 20 | 21 | func TestFlagErrors(t *testing.T) { 22 | for _, tt := range []struct { 23 | input []string 24 | err error 25 | }{ 26 | { 27 | input: []string{"-build=else", "-format=cpio"}, 28 | err: os.ErrInvalid, 29 | }, 30 | { 31 | input: []string{"-format=else", "-build=bb"}, 32 | err: os.ErrInvalid, 33 | }, 34 | } { 35 | fs := flag.NewFlagSet("test", flag.ContinueOnError) 36 | f := &Flags{} 37 | f.RegisterFlags(fs) 38 | if err := fs.Parse(tt.input); err != nil { 39 | t.Fatal(err) 40 | } 41 | if _, err := f.Modifiers(); !errors.Is(err, tt.err) { 42 | t.Errorf("Modifiers = %v, want %v", err, tt.err) 43 | } 44 | } 45 | } 46 | 47 | func TestFlags(t *testing.T) { 48 | for _, tt := range []struct { 49 | input []string 50 | want *Flags 51 | }{ 52 | { 53 | input: []string{"-build=bb", "-initcmd=foo"}, 54 | want: &Flags{ 55 | Init: String("foo"), 56 | Commands: CommandFlags{ 57 | Builder: "bb", 58 | BuildOpts: &golang.BuildOpts{}, 59 | }, 60 | }, 61 | }, 62 | { 63 | input: []string{"-build=bb"}, 64 | want: &Flags{ 65 | Commands: CommandFlags{ 66 | Builder: "bb", 67 | BuildOpts: &golang.BuildOpts{}, 68 | }, 69 | }, 70 | }, 71 | { 72 | input: []string{"-build=bb", "-initcmd=foo", "-uinitcmd=foo bar", "-tmp-dir=bla", "-defaultsh=gosh"}, 73 | want: &Flags{ 74 | Init: String("foo"), 75 | Uinit: String("foo bar"), 76 | TempDir: String("bla"), 77 | Shell: String("gosh"), 78 | Commands: CommandFlags{ 79 | Builder: "bb", 80 | BuildOpts: &golang.BuildOpts{}, 81 | }, 82 | }, 83 | }, 84 | { 85 | input: []string{"-build=bb", "-initcmd=", "-uinitcmd=", "-tmp-dir=", "-defaultsh="}, 86 | want: &Flags{ 87 | Init: String(""), 88 | Uinit: String(""), 89 | TempDir: String(""), 90 | Shell: String(""), 91 | Commands: CommandFlags{ 92 | Builder: "bb", 93 | BuildOpts: &golang.BuildOpts{}, 94 | }, 95 | }, 96 | }, 97 | } { 98 | fs := flag.NewFlagSet("test", flag.ContinueOnError) 99 | f := &Flags{} 100 | f.RegisterFlags(fs) 101 | if err := fs.Parse(tt.input); err != nil { 102 | t.Fatal(err) 103 | } 104 | if !reflect.DeepEqual(f, tt.want) { 105 | t.Errorf("Parse = %+v, want %+v", f, tt.want) 106 | } 107 | } 108 | } 109 | 110 | func TestFlagModifiers(t *testing.T) { 111 | for _, tt := range []struct { 112 | input []string 113 | base []uimage.Modifier 114 | want *uimage.Opts 115 | cmds []string 116 | }{ 117 | { 118 | // Override modifier defaults with empty. 119 | input: []string{"-build=bb", "-format=cpio", "-initcmd=", "-uinitcmd=", "-defaultsh=", "-tmp-dir="}, 120 | base: []uimage.Modifier{ 121 | uimage.WithInit("foo"), 122 | uimage.WithUinit("foo bar"), 123 | uimage.WithTempDir("foo"), 124 | uimage.WithShell("foo"), 125 | }, 126 | cmds: []string{"foo"}, 127 | want: &uimage.Opts{ 128 | Env: golang.Default(), 129 | Commands: []uimage.Commands{ 130 | { 131 | Builder: &builder.GBBBuilder{}, 132 | BuildOpts: &golang.BuildOpts{}, 133 | }, 134 | }, 135 | }, 136 | }, 137 | } { 138 | fs := flag.NewFlagSet("test", flag.ContinueOnError) 139 | f := &Flags{} 140 | f.RegisterFlags(fs) 141 | if err := fs.Parse(tt.input); err != nil { 142 | t.Fatal(err) 143 | } 144 | mods, err := f.Modifiers() 145 | if err != nil { 146 | t.Errorf("Modifiers = %v", err) 147 | } 148 | opts, err := uimage.OptionsFor(append(tt.base, mods...)...) 149 | if err != nil { 150 | t.Errorf("Options = %v", err) 151 | } 152 | if diff := cmp.Diff(opts, tt.want, cmpopts.IgnoreFields(uimage.Opts{}, "BaseArchive")); diff != "" { 153 | t.Errorf("opts (-got, +want) = %v", diff) 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /uimage/templates/templates.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 templates defines a uimage template configuration file parser. 6 | package templates 7 | 8 | import ( 9 | "errors" 10 | "fmt" 11 | "os" 12 | "path/filepath" 13 | 14 | "github.com/u-root/gobusybox/src/pkg/golang" 15 | "github.com/u-root/mkuimage/uimage" 16 | "gopkg.in/yaml.v3" 17 | ) 18 | 19 | // ErrTemplateNotExist is returned when the given config name did not exist. 20 | var ErrTemplateNotExist = errors.New("config template does not exist") 21 | 22 | func findConfigFile(name string) (string, error) { 23 | dir, err := os.Getwd() 24 | if err != nil { 25 | return "", err 26 | } 27 | for dir != "/" { 28 | p := filepath.Join(dir, name) 29 | if _, err := os.Stat(p); err == nil { 30 | return p, nil 31 | } 32 | dir = filepath.Dir(dir) 33 | } 34 | return "", fmt.Errorf("%w: could not find %s in current directory or any parent", os.ErrNotExist, name) 35 | } 36 | 37 | // Command represents commands to build. 38 | type Command struct { 39 | // Builder is bb, gbb, or binary. 40 | // 41 | // Defaults to bb if not given. 42 | Builder string 43 | 44 | // Commands are commands or template names. 45 | Commands []string 46 | } 47 | 48 | // Config is a mkuimage build configuration. 49 | type Config struct { 50 | GOOS string 51 | GOARCH string 52 | BuildTags []string `yaml:"build_tags"` 53 | Commands []Command 54 | Files []string 55 | Init *string 56 | Uinit *string 57 | Shell *string 58 | } 59 | 60 | // Templates are a set of mkuimage build configs and command templates. 61 | type Templates struct { 62 | Configs map[string]Config 63 | 64 | // Commands defines a set of command template name -> commands to expand. 65 | Commands map[string][]string 66 | } 67 | 68 | // Uimage returns the uimage modifiers for the given templated config name. 69 | func (t *Templates) Uimage(config string) ([]uimage.Modifier, error) { 70 | if config == "" { 71 | return nil, nil 72 | } 73 | if t == nil { 74 | return nil, fmt.Errorf("%w: no templates parsed", ErrTemplateNotExist) 75 | } 76 | c, ok := t.Configs[config] 77 | if !ok { 78 | return nil, fmt.Errorf("%w: %q", ErrTemplateNotExist, config) 79 | } 80 | m := []uimage.Modifier{ 81 | uimage.WithFiles(c.Files...), 82 | uimage.WithEnv( 83 | golang.WithGOOS(c.GOOS), 84 | golang.WithGOARCH(c.GOARCH), 85 | golang.WithBuildTag(c.BuildTags...), 86 | ), 87 | } 88 | if c.Init != nil { 89 | m = append(m, uimage.WithInit(*c.Init)) 90 | } 91 | if c.Uinit != nil { 92 | m = append(m, uimage.WithUinitCommand(*c.Uinit)) 93 | } 94 | if c.Shell != nil { 95 | m = append(m, uimage.WithShell(*c.Shell)) 96 | } 97 | for _, cmds := range c.Commands { 98 | switch cmds.Builder { 99 | case "binary": 100 | m = append(m, uimage.WithBinaryCommands(t.CommandsFor(cmds.Commands...)...)) 101 | case "bb", "gbb": 102 | fallthrough 103 | default: 104 | m = append(m, uimage.WithBusyboxCommands(t.CommandsFor(cmds.Commands...)...)) 105 | } 106 | } 107 | return m, nil 108 | } 109 | 110 | // CommandsFor expands commands according to command templates. 111 | func (t *Templates) CommandsFor(names ...string) []string { 112 | if t == nil { 113 | return names 114 | } 115 | var c []string 116 | for _, name := range names { 117 | cmds, ok := t.Commands[name] 118 | if ok { 119 | c = append(c, cmds...) 120 | } else { 121 | c = append(c, name) 122 | } 123 | } 124 | return c 125 | } 126 | 127 | // TemplateFrom parses a template from bytes. 128 | func TemplateFrom(b []byte) (*Templates, error) { 129 | var tpl Templates 130 | if err := yaml.Unmarshal(b, &tpl); err != nil { 131 | return nil, err 132 | } 133 | return &tpl, nil 134 | } 135 | 136 | // Template parses the first file named .mkuimage.yaml in the current directory or any of its parents. 137 | func Template() (*Templates, error) { 138 | p, err := findConfigFile(".mkuimage.yaml") 139 | if err != nil { 140 | return nil, fmt.Errorf("%w: no templates found", os.ErrNotExist) 141 | } 142 | return TemplateFromFile(p) 143 | } 144 | 145 | // TemplateFromFile parses a template from the given file. 146 | func TemplateFromFile(p string) (*Templates, error) { 147 | b, err := os.ReadFile(p) 148 | if err != nil { 149 | return nil, err 150 | } 151 | return TemplateFrom(b) 152 | } 153 | -------------------------------------------------------------------------------- /uimage/templates/templates_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2024 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 templates 6 | 7 | import ( 8 | "errors" 9 | "os" 10 | "path/filepath" 11 | "reflect" 12 | "testing" 13 | 14 | "github.com/u-root/gobusybox/src/pkg/golang" 15 | "github.com/u-root/mkuimage/uimage" 16 | "github.com/u-root/mkuimage/uimage/builder" 17 | ) 18 | 19 | func TestMods(t *testing.T) { 20 | for _, tt := range []struct { 21 | name string 22 | tpl string 23 | config string 24 | base []uimage.Modifier 25 | want *uimage.Opts 26 | err error 27 | }{ 28 | { 29 | name: "ok", 30 | tpl: ` 31 | commands: 32 | core: 33 | - github.com/u-root/u-root/cmds/core/ip 34 | - github.com/u-root/u-root/cmds/core/init 35 | - github.com/u-root/u-root/cmds/core/gosh 36 | 37 | minimal: 38 | - github.com/u-root/u-root/cmds/core/ls 39 | - github.com/u-root/u-root/cmds/core/init 40 | 41 | configs: 42 | plan9: 43 | goarch: amd64 44 | goos: plan9 45 | build_tags: [grpcnotrace] 46 | files: 47 | - /bin/bash 48 | init: init 49 | uinit: gosh script.sh 50 | shell: gosh 51 | commands: 52 | - builder: bb 53 | commands: [core, minimal] 54 | - builder: bb 55 | commands: [./u-bmc/cmd/foo] 56 | - builder: binary 57 | commands: [./u-bmc/cmd/bar] 58 | - builder: binary 59 | commands: [cmd/test2json] 60 | `, 61 | config: "plan9", 62 | want: &uimage.Opts{ 63 | Env: golang.Default(golang.WithGOARCH("amd64"), golang.WithGOOS("plan9"), golang.WithBuildTag("grpcnotrace")), 64 | ExtraFiles: []string{"/bin/bash"}, 65 | InitCmd: "init", 66 | UinitCmd: "gosh", 67 | UinitArgs: []string{"script.sh"}, 68 | DefaultShell: "gosh", 69 | Commands: []uimage.Commands{ 70 | { 71 | Builder: builder.Busybox, 72 | Packages: []string{ 73 | "github.com/u-root/u-root/cmds/core/ip", 74 | "github.com/u-root/u-root/cmds/core/init", 75 | "github.com/u-root/u-root/cmds/core/gosh", 76 | "github.com/u-root/u-root/cmds/core/ls", 77 | "github.com/u-root/u-root/cmds/core/init", 78 | "./u-bmc/cmd/foo", 79 | }, 80 | }, 81 | { 82 | Builder: builder.Binary, 83 | Packages: []string{"./u-bmc/cmd/bar"}, 84 | }, 85 | { 86 | Builder: builder.Binary, 87 | Packages: []string{"cmd/test2json"}, 88 | }, 89 | }, 90 | }, 91 | }, 92 | { 93 | name: "missing_config", 94 | tpl: ` 95 | configs: 96 | plan9: 97 | goarch: amd64 98 | goos: plan9 99 | `, 100 | config: "plan10", 101 | err: ErrTemplateNotExist, 102 | }, 103 | { 104 | name: "no config", 105 | tpl: ` 106 | configs: 107 | plan9: 108 | goarch: amd64 109 | goos: plan9 110 | `, 111 | config: "", 112 | }, 113 | { 114 | name: "override base", 115 | tpl: ` 116 | configs: 117 | noinit: 118 | init: "" 119 | `, 120 | config: "noinit", 121 | base: []uimage.Modifier{ 122 | uimage.WithInit("init"), 123 | }, 124 | want: &uimage.Opts{ 125 | Env: golang.Default(), 126 | }, 127 | }, 128 | } { 129 | t.Run(tt.name, func(t *testing.T) { 130 | tpl, err := TemplateFrom([]byte(tt.tpl)) 131 | if err != nil { 132 | t.Fatal(err) 133 | } 134 | mods, err := tpl.Uimage(tt.config) 135 | if !errors.Is(err, tt.err) { 136 | t.Fatalf("UimageMods = %v, want %v", err, tt.err) 137 | } 138 | if len(mods) == 0 { 139 | return 140 | } 141 | got, err := uimage.OptionsFor(append(tt.base, mods...)...) 142 | if err != nil { 143 | t.Fatal(err) 144 | } 145 | if !reflect.DeepEqual(got, tt.want) { 146 | t.Logf("got: %#v", got) 147 | t.Logf("want: %#v", tt.want) 148 | t.Errorf("not equal") 149 | } 150 | }) 151 | } 152 | } 153 | 154 | func TestTemplateErr(t *testing.T) { 155 | if _, err := TemplateFrom([]byte("\t")); err == nil { 156 | t.Fatal("Expected error") 157 | } 158 | 159 | d := t.TempDir() 160 | wd, _ := os.Getwd() 161 | _ = os.Chdir(d) 162 | defer func() { _ = os.Chdir(wd) }() 163 | 164 | if _, err := Template(); !errors.Is(err, os.ErrNotExist) { 165 | t.Fatalf("Template = %v, want ErrNotExist", err) 166 | } 167 | if _, err := TemplateFromFile(filepath.Join(d, "foobar")); !os.IsNotExist(err) { 168 | t.Fatalf("Template = %v, want not exist", err) 169 | } 170 | } 171 | --------------------------------------------------------------------------------