diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..fcf00c2 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,142 @@ +name: Build + +on: + push: + branches: + - 'master' + tags: + - '*' + schedule: + - cron: '40 4 * * *' # every day at 4:40 + pull_request: + +jobs: + test: + name: "Test" + + strategy: + fail-fast: false + matrix: + platform: [ + ubuntu-latest, + macos-latest, + windows-latest + ] + + runs-on: ${{ matrix.platform }} + timeout-minutes: 15 + + steps: + - name: "Checkout Repository" + uses: actions/checkout@v1 + + - name: Set up Rustup + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + + - name: "Print Rust Version" + run: | + rustc -Vv + cargo -Vv + + - name: "Run cargo build" + uses: actions-rs/cargo@v1 + with: + command: build + + - name: "Run cargo test" + uses: actions-rs/cargo@v1 + with: + command: test + + - name: "Deny Warnings" + uses: actions-rs/cargo@v1 + with: + command: build + env: + RUSTFLAGS: "-D warnings" + + - name: "Install it" + run: cargo install --path . + + - name: "Switch to Rust nightly" + run: rustup default nightly + + - name: "Install Rustup Components" + run: rustup component add rust-src llvm-tools-preview + + # install QEMU + - name: Install QEMU (Linux) + run: | + sudo apt update + sudo apt install qemu-system-x86 + if: runner.os == 'Linux' + - name: Install QEMU (macOS) + run: brew install qemu + if: runner.os == 'macOS' + env: + HOMEBREW_NO_AUTO_UPDATE: 1 + HOMEBREW_NO_BOTTLE_SOURCE_FALLBACK: 1 + HOMEBREW_NO_INSTALL_CLEANUP: 1 + - name: Install QEMU (Windows) + run: | + choco install qemu --version 2021.5.5 + echo "$Env:Programfiles\qemu" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + if: runner.os == 'Windows' + shell: pwsh + + - name: "Print QEMU Version" + run: qemu-system-x86_64 --version + + - name: 'Build "basic" Kernel' + run: cargo bootimage --target ../x86_64-bootimage-example-kernels.json + working-directory: example-kernels/basic + + - name: 'Run QEMU with "basic" Kernel' + run: | + qemu-system-x86_64 -drive format=raw,file=target/x86_64-bootimage-example-kernels/debug/bootimage-basic.bin -device isa-debug-exit,iobase=0xf4,iosize=0x04 -display none + if [ $? -eq 103 ]; then (exit 0); else (exit 1); fi + shell: bash {0} + working-directory: example-kernels + + - name: 'Run `cargo run` for "runner" kernel' + run: | + cargo run + if [ $? -eq 109 ]; then (exit 0); else (exit 1); fi + shell: bash {0} + working-directory: example-kernels/runner + + - run: cargo test + working-directory: example-kernels/runner-test + name: 'Run `cargo test` for "runner-test" kernel' + + - run: cargo test -Z doctest-xcompile + working-directory: example-kernels/runner-doctest + name: 'Run `cargo test -Z doctest-xcompile` for "runner-doctest" kernel' + + - run: cargo test + working-directory: example-kernels/runner-fail-reboot + name: 'Run `cargo test` for "runner-fail-reboot" kernel' + + check_formatting: + name: "Check Formatting" + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - uses: actions/checkout@v1 + - run: rustup toolchain install nightly --profile minimal --component rustfmt + - run: cargo +nightly fmt -- --check + + clippy: + name: "Clippy" + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v1 + - run: rustup toolchain install nightly --profile minimal --component clippy + - name: "Run `cargo clippy`" + uses: actions-rs/cargo@v1 + with: + command: clippy diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index dff6ebf..0000000 --- a/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: rust - -sudo: false - -notifications: - email: - on_success: never - on_failure: change - -rust: - - nightly - -cache: cargo - -script: -- cargo test diff --git a/Cargo.lock b/Cargo.lock index a860c59..b31b442 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,304 +1,201 @@ -[[package]] -name = "backtrace" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)", - "cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", - "rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "backtrace-sys" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", -] +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 [[package]] -name = "bitflags" -version = "1.0.1" +name = "anyhow" +version = "1.0.99" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0674a1ddeecb70197781e945de4b3b8ffb61fa939a5597bcf48503737663100" [[package]] name = "bootimage" -version = "0.3.0" +version = "0.10.3" dependencies = [ - "byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "cargo_metadata 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)", - "tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)", - "toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)", - "wait-timeout 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)", - "xmas-elf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)", + "anyhow", + "cargo_metadata", + "json", + "llvm-tools", + "locate-cargo-manifest", + "thiserror", + "toml", + "wait-timeout", ] -[[package]] -name = "byteorder" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - [[package]] name = "cargo_metadata" -version = "0.5.4" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e3374c604fb39d1a2f35ed5e4a4e30e60d01fab49446e08f1b3e9a90aef202" dependencies = [ - "error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", - "semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)", + "semver", + "serde", + "serde_derive", + "serde_json", ] [[package]] -name = "cc" -version = "1.0.9" +name = "itoa" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] -name = "cfg-if" -version = "0.1.2" +name = "json" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" [[package]] -name = "dtoa" -version = "0.4.2" +name = "libc" +version = "0.2.175" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543" [[package]] -name = "error-chain" -version = "0.11.0" +name = "llvm-tools" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)", -] +checksum = "955be5d0ca0465caf127165acb47964f911e2bc26073e865deb8be7189302faf" [[package]] -name = "fuchsia-zircon" -version = "0.3.3" +name = "locate-cargo-manifest" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db985b63431fe09e8d71f50aeceffcc31e720cb86be8dad2f38d084c5a328466" dependencies = [ - "bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", + "json", ] [[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "itoa" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "libc" -version = "0.2.40" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "num-traits" -version = "0.2.2" +name = "memchr" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "proc-macro2" -version = "0.3.6" +version = "1.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" dependencies = [ - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "unicode-ident", ] [[package]] name = "quote" -version = "0.5.1" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" dependencies = [ - "proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", ] [[package]] -name = "rand" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", - "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "remove_dir_all" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "rustc-demangle" -version = "0.1.7" +name = "ryu" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" [[package]] name = "semver" version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" dependencies = [ - "semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", + "semver-parser", + "serde", ] [[package]] name = "semver-parser" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" [[package]] name = "serde" -version = "1.0.37" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "serde_derive" -version = "1.0.37" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" dependencies = [ - "proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "serde_derive_internals 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_derive", ] [[package]] -name = "serde_derive_internals" -version = "0.23.0" +name = "serde_derive" +version = "1.0.219" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" dependencies = [ - "proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] name = "serde_json" -version = "1.0.13" +version = "1.0.142" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" dependencies = [ - "dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)", - "num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", + "itoa", + "memchr", + "ryu", + "serde", ] [[package]] name = "syn" -version = "0.13.1" +version = "2.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" dependencies = [ - "proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)", - "quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", - "unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] -name = "tempdir" -version = "0.3.7" +name = "thiserror" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" dependencies = [ - "rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)", - "remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror-impl", ] [[package]] -name = "toml" -version = "0.4.6" +name = "thiserror-impl" +version = "1.0.69" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" dependencies = [ - "serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)", + "proc-macro2", + "quote", + "syn", ] [[package]] -name = "unicode-xid" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "wait-timeout" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -dependencies = [ - "libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)", -] - -[[package]] -name = "winapi" -version = "0.3.4" +name = "toml" +version = "0.5.11" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" dependencies = [ - "winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", - "winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde", ] [[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" +name = "unicode-ident" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[[package]] -name = "xmas-elf" -version = "0.6.2" +name = "wait-timeout" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" dependencies = [ - "zero 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)", + "libc", ] - -[[package]] -name = "zero" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" - -[metadata] -"checksum backtrace 0.3.5 (registry+https://github.com/rust-lang/crates.io-index)" = "ebbbf59b1c43eefa8c3ede390fcc36820b4999f7914104015be25025e0d62af2" -"checksum backtrace-sys 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "44585761d6161b0f57afc49482ab6bd067e4edef48c12a152c237eb0203f7661" -"checksum bitflags 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "b3c30d3802dfb7281680d6285f2ccdaa8c2d8fee41f93805dba5c4cf50dc23cf" -"checksum byteorder 1.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "73b5bdfe7ee3ad0b99c9801d58807a9dbc9e09196365b0203853b99889ab3c87" -"checksum cargo_metadata 0.5.4 (registry+https://github.com/rust-lang/crates.io-index)" = "6ebd6272a2ca4fd39dbabbd6611eb03df45c2259b3b80b39a9ff8fbdcf42a4b3" -"checksum cc 1.0.9 (registry+https://github.com/rust-lang/crates.io-index)" = "2b4911e4bdcb4100c7680e7e854ff38e23f1b34d4d9e079efae3da2801341ffc" -"checksum cfg-if 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "d4c819a1287eb618df47cc647173c5c4c66ba19d888a6e50d605672aed3140de" -"checksum dtoa 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "09c3753c3db574d215cba4ea76018483895d7bff25a31b49ba45db21c48e50ab" -"checksum error-chain 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ff511d5dc435d703f4971bc399647c9bc38e20cb41452e3b9feb4765419ed3f3" -"checksum fuchsia-zircon 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" -"checksum fuchsia-zircon-sys 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" -"checksum itoa 0.4.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c069bbec61e1ca5a596166e55dfe4773ff745c3d16b700013bcaff9a6df2c682" -"checksum libc 0.2.40 (registry+https://github.com/rust-lang/crates.io-index)" = "6fd41f331ac7c5b8ac259b8bf82c75c0fb2e469bbf37d2becbba9a6a2221965b" -"checksum num-traits 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "dee092fcdf725aee04dd7da1d21debff559237d49ef1cb3e69bcb8ece44c7364" -"checksum proc-macro2 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)" = "49b6a521dc81b643e9a51e0d1cf05df46d5a2f3c0280ea72bcb68276ba64a118" -"checksum quote 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "7b0ff51282f28dc1b53fd154298feaa2e77c5ea0dba68e1fd8b03b72fbe13d2a" -"checksum rand 0.4.2 (registry+https://github.com/rust-lang/crates.io-index)" = "eba5f8cb59cc50ed56be8880a5c7b496bfd9bd26394e176bc67884094145c2c5" -"checksum remove_dir_all 0.5.1 (registry+https://github.com/rust-lang/crates.io-index)" = "3488ba1b9a2084d38645c4c08276a1752dcbf2c7130d74f1569681ad5d2799c5" -"checksum rustc-demangle 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)" = "11fb43a206a04116ffd7cfcf9bcb941f8eb6cc7ff667272246b0a1c74259a3cb" -"checksum semver 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1d7eb9ef2c18661902cc47e535f9bc51b78acd254da71d375c2f6720d9a40403" -"checksum semver-parser 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "388a1df253eca08550bef6c72392cfe7c30914bf41df5269b68cbd6ff8f570a3" -"checksum serde 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "d3bcee660dcde8f52c3765dd9ca5ee36b4bf35470a738eb0bd5a8752b0389645" -"checksum serde_derive 1.0.37 (registry+https://github.com/rust-lang/crates.io-index)" = "f1711ab8b208541fa8de00425f6a577d90f27bb60724d2bb5fd911314af9668f" -"checksum serde_derive_internals 0.23.0 (registry+https://github.com/rust-lang/crates.io-index)" = "89b340a48245bc03ddba31d0ff1709c118df90edc6adabaca4aac77aea181cce" -"checksum serde_json 1.0.13 (registry+https://github.com/rust-lang/crates.io-index)" = "5c508584d9913df116b91505eec55610a2f5b16e9ed793c46e4d0152872b3e74" -"checksum syn 0.13.1 (registry+https://github.com/rust-lang/crates.io-index)" = "91b52877572087400e83d24b9178488541e3d535259e04ff17a63df1e5ceff59" -"checksum tempdir 0.3.7 (registry+https://github.com/rust-lang/crates.io-index)" = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" -"checksum toml 0.4.6 (registry+https://github.com/rust-lang/crates.io-index)" = "a0263c6c02c4db6c8f7681f9fd35e90de799ebd4cfdeab77a38f4ff6b3d8c0d9" -"checksum unicode-xid 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc" -"checksum wait-timeout 0.1.5 (registry+https://github.com/rust-lang/crates.io-index)" = "b9f3bf741a801531993db6478b95682117471f76916f5e690dd8d45395b09349" -"checksum winapi 0.3.4 (registry+https://github.com/rust-lang/crates.io-index)" = "04e3bd221fcbe8a271359c04f21a76db7d0c6028862d1bb5512d85e1e2eb5bb3" -"checksum winapi-i686-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -"checksum winapi-x86_64-pc-windows-gnu 0.4.0 (registry+https://github.com/rust-lang/crates.io-index)" = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -"checksum xmas-elf 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "22678df5df766e8d1e5d609da69f0c3132d794edf6ab5e75e7abcd2270d4cf58" -"checksum zero 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "5f1bc8a6b2005884962297587045002d8cfb8dcec9db332f4ca216ddc5de82c5" diff --git a/Cargo.toml b/Cargo.toml index eef7c77..f2fbf21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,13 +3,23 @@ authors = ["Philipp Oppermann "] description = "Tool to create a bootable OS image from a kernel binary." license = "MIT/Apache-2.0" name = "bootimage" -version = "0.3.0" +version = "0.10.3" repository = "https://github.com/rust-osdev/bootimage" +edition = "2018" [dependencies] -byteorder = "1.2.1" -toml = "0.4.5" -xmas-elf = "0.6.1" -cargo_metadata = "0.5.3" -tempdir = "0.3.7" -wait-timeout = "0.1" +toml = "0.5.6" +wait-timeout = "0.2.0" +llvm-tools = "0.1.1" +locate-cargo-manifest = "0.2.0" +json = "0.12.4" +anyhow = "1.0.28" +thiserror = "1.0.16" +cargo_metadata = "0.9.1" + +[package.metadata.release] +no-dev-version = true +pre-release-replacements = [ + { file="Changelog.md", search="# Unreleased", replace="# Unreleased\n\n# {{version}} – {{date}}", exactly=1 }, +] +pre-release-commit-message = "Release version {{version}}" diff --git a/Changelog.md b/Changelog.md new file mode 100644 index 0000000..63326a4 --- /dev/null +++ b/Changelog.md @@ -0,0 +1,144 @@ +# Unreleased + +# 0.10.3 – 2021-04-01 + +- Fix "unnnecessary trailing semicolon" warning on Rust 1.51 + +# 0.10.2 – 2020-12-10 + +- Fix nightly breakage of doctests in workspaces ([#69](https://github.com/rust-osdev/bootimage/pull/69)) + +# 0.10.1 – 2020-08-03 + +- Parse `--version` argument without subcommand (`bootimage --version`) ([#67](https://github.com/rust-osdev/bootimage/pull/67)) + +# 0.10.0 – 2020-08-03 + +- **Breaking:** Consider all other exit codes besides 'test-success-exit-code' as failures ([#65](https://github.com/rust-osdev/bootimage/pull/65)) + - Also runs tests with `-no-reboot` by default, configurable through a new `test-no-reboot` config key + +# 0.9.0 – 2020-07-17 + +- **Breaking:** Make `cargo bootimage` use `cargo build` instead of `cargo xbuild` ([#63](https://github.com/rust-osdev/bootimage/pull/63)) + +# 0.8.1 – 2020-07-17 + +- Add support for building bootloaders using `-Zbuild-std ([#62](https://github.com/rust-osdev/bootimage/pull/62)) + +# 0.8.0 + +- **Breaking:** Rewrite: Remove support for `bootimage {run, test}` ([#55](https://github.com/rust-osdev/bootimage/pull/55)) + +# 0.7.10 + +- Add support for doctests ([#52](https://github.com/rust-osdev/bootimage/pull/52)) + +# 0.7.9 + +- Set empty RUSTFLAGS to ensure that no .cargo/config applies ([#51](https://github.com/rust-osdev/bootimage/pull/51)) + +# 0.7.8 + +- Don't exit with expected exit code when failed to read QEMU exit code ([#47](https://github.com/rust-osdev/bootimage/pull/47)) + +# 0.7.7 + +- Pass location of kernel's Cargo.toml to bootloader ([#45](https://github.com/rust-osdev/bootimage/pull/45)) + +# 0.7.6 + +- If the bootloader has a feature named `binary`, enable it ([#43](https://github.com/rust-osdev/bootimage/pull/43)) + +# 0.7.5 + +- Set XBUILD_SYSROOT_PATH when building bootloader ([#41](https://github.com/rust-osdev/bootimage/pull/41)) +- Update Azure Pipelines CI script ([#40](https://github.com/rust-osdev/bootimage/pull/40)) + +# 0.7.4 + +- Align boot image size on a 512 byte boundary to fix boot in VirtualBox (see [#35](https://github.com/rust-osdev/bootimage/issues/35)) + +# 0.7.3 + +- Fix `cargo bootimage` on Windows (there was a bug in the argument parsing) + +# 0.7.2 + +- New features for `bootimage runner` + - Pass additional arguments to the run command (e.g. QEMU) + - Consider all binaries in the `target/deps` folder as test executables + - Apply `test-timeout` config key when running tests in `bootimage runner` + - Don't apply `run-args` for test executables + - Add a new `test-args` config key for test arguments + - Add a new `test-success-exit-code` config key for interpreting an exit code as success + - This is useful when the `isa-debug-exit` QEMU device is used. + - Improve printing of the run command (print string instead of array, print non-canonicalized executable path, respect `--quiet`) + +# 0.7.1 + +- Fix for backwards compatibility: Ignore `test-` executables for `bootimage run`. + - This ensures that `bootimage run` still works without the need for a `--bin` argument if all other executables are integration tests. + - This only changes the default, you can still run test executables by passing `--bin test-.` + +# 0.7.0 + +## Breaking + +- Rewrite for new bootloader build system + - Compatible with bootloader 0.5.1+ +- Remove the following config options: `output`, `bootloader.*`, `minimum_image_size`, and `package_filepath` + - The bootloader is now fully controlled through cargo dependencies. + - For using a bootloader crate with name different than `bootloader` use [cargo's rename feature](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#renaming-dependencies-in-cargotoml). +- Remove support for `bootloader_precompiled` + - The `bootloader` crate compiles fine on all architectures for some time and should be prefered +- Require the `llvm-tools-preview` rustup component +- Pass the QEMU exit code in `bootimage run` + +## Other + +- Add support for default targets declared in `.cargo/config` files +- Add a `cargo-bootimage` executable that is equivalent to `bootimage build` and can be used as cargo subcommand (`cargo bootimage`) +- Add a new `bootimage runner` subcommand that can be used as `target.[…].runner` in `.cargo/config` files +- Make test timeout configurable and increase default to 5 minutes +- Move crate to 2018 edition +- Refactor and cleanup the code +- Remove the dependency on `failure` + - Use a custom `ErrorMessage` type instead +- Add a new `run-args` config key +- Add a new `--quiet` argument to suppress output + +# 0.6.6 + +- Update dependencies + +# 0.6.5 + +- You can now mark integration tests as success/failure by setting the exit code in the QEMU `isa-debug-exit` device. See [#32](https://github.com/rust-osdev/bootimage/issues/32) for more information. + +# 0.6.4 + +- Canonicalize paths before comparing them when invoking `bootimage test` + - This caused an error on Windows where the path in the cargo metadata is not fully canonicalized +- Improve CI infrastructure + +# 0.6.3 + +- Canonicalize paths before comparing them when invoking `bootimage build` + - This caused an error on Windows where the path in the cargo metadata is not fully canonicalized + +# 0.6.2 + +- Fix build on Windows (don't use the `.` directory) + +# 0.6.1 + +- Fix: bootimage should now work correctly with `--manifest-path` + +# 0.6.0 + +(Yanked from crates.io because of a bug fixed in 0.6.1.) + +**Breaking**: + +- When no `--manifest-path` argument is passed, `bootimage` defaults to the `Cargo.toml` in the current directory instead of the workspace root. + - This fixes compilation of projects that are part of a workspace diff --git a/Readme.md b/Readme.md index 76abb69..852f25d 100644 --- a/Readme.md +++ b/Readme.md @@ -10,43 +10,91 @@ Creates a bootable disk image from a Rust OS kernel. ## Usage -To build the kernel project and create a bootable disk image from it, run: +First you need to add a dependency on the [`bootloader`](https://github.com/rust-osdev/bootloader) crate: + +```toml +# in your Cargo.toml + +[dependencies] +bootloader = "0.9.8" +``` + +**Note**: At least bootloader version `0.5.1` is required since `bootimage 0.7.0`. For earlier bootloader versions, use `bootimage 0.6.6`. + +If you want to use a custom bootloader with a different name, you can use Cargo's [rename functionality](https://doc.rust-lang.org/cargo/reference/specifying-dependencies.html#renaming-dependencies-in-cargotoml). + +### Building + +Now you can build the kernel project and create a bootable disk image from it by running: ``` -> bootimage build --target your_custom_target.json [other_args] +cargo bootimage --target your_custom_target.json [other_args] ``` -The command will invoke [`cargo xbuild`](https://github.com/rust-osdev/cargo-xbuild), forwarding all passed options. Then it will download and build a bootloader, by default the [rust-osdev/bootloader](https://github.com/rust-osdev/bootloader). Finally, it combines the kernel and the bootloader into a bootable disk image. +The command will invoke `cargo build`, forwarding all passed options. Then it will build the specified bootloader together with the kernel to create a bootable disk image. -## Configuration +### Running -Configuration is done through a through a `[package.metadata.bootimage]` table in the `Cargo.toml`. The following options are available: +To run your kernel in QEMU, you can set a `bootimage runner` as a custom runner in a `.cargo/config` file: ```toml - [package.metadata.bootimage] - default-target = "" # This target is used if no `--target` is passed - output = "bootimage.bin" # The output file name - minimum-image-size = 0 # The minimum output file size (in MiB) - # The command invoked on `bootimage run` - # (the "{}" will be replaced with the path to the bootable disk image) - run-command = ["qemu-system-x86_64", "-drive", "format=raw,file={}"] - - [package.metadata.bootimage.bootloader] - name = "bootloader" # The bootloader crate name - version = "" # The bootloader version that should be used - git = "" # Use the bootloader from this git repository - branch = "" # The git branch to use (defaults to master) - path = "" # Use the bootloader from this local path - precompiled = false # Whether the bootloader crate is precompiled - target = "x86_64-bootloader.json" # Target triple for compiling the bootloader -``` - -If no `[package.metadata.bootimage.bootloader]` sub-table is specified, it defaults to: +[target.'cfg(target_os = "none")'] +runner = "bootimage runner" +``` + +Then you can run your kernel through: + +``` +cargo xrun --target your_custom_target.json [other_args] -- [qemu args] +``` + +All arguments after `--` are passed to QEMU. If you want to use a custom run command, see the _Configuration_ section below. + +### Testing + +The `bootimage` has built-in support for running unit and integration tests of your kernel. For this, you need to use the `custom_tests_framework` feature of Rust as described [here](https://os.phil-opp.com/testing/#custom-test-frameworks). + +## Configuration + +Configuration is done through a `[package.metadata.bootimage]` table in the `Cargo.toml` of your kernel. The following options are available: ```toml -name = "bootloader_precompiled" -precompiled = true +[package.metadata.bootimage] +# The cargo subcommand that will be used for building the kernel. +# +# For building using the `cargo-xbuild` crate, set this to `xbuild`. +build-command = ["build"] +# The command invoked with the created bootimage (the "{}" will be replaced +# with the path to the bootable disk image) +# Applies to `bootimage run` and `bootimage runner` +run-command = ["qemu-system-x86_64", "-drive", "format=raw,file={}"] + +# Additional arguments passed to the run command for non-test executables +# Applies to `bootimage run` and `bootimage runner` +run-args = [] + +# Additional arguments passed to the run command for test executables +# Applies to `bootimage runner` +test-args = [] + +# An exit code that should be considered as success for test executables +test-success-exit-code = {integer} + +# The timeout for running a test through `bootimage test` or `bootimage runner` (in seconds) +test-timeout = 300 + +# Whether the `-no-reboot` flag should be passed to test executables +test-no-reboot = true ``` ## License -Dual-licensed under MIT or the Apache License (Version 2.0). + +Licensed under either of + +- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or + http://www.apache.org/licenses/LICENSE-2.0) +- MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + +at your option. + +Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. diff --git a/example-kernels/.cargo/config.toml b/example-kernels/.cargo/config.toml new file mode 100644 index 0000000..92cee48 --- /dev/null +++ b/example-kernels/.cargo/config.toml @@ -0,0 +1,2 @@ +[unstable] +build-std = ["core", "compiler_builtins"] diff --git a/example-kernels/.gitignore b/example-kernels/.gitignore new file mode 100644 index 0000000..eccd7b4 --- /dev/null +++ b/example-kernels/.gitignore @@ -0,0 +1,2 @@ +/target/ +**/*.rs.bk diff --git a/example-kernels/Cargo.toml b/example-kernels/Cargo.toml new file mode 100644 index 0000000..e20e2aa --- /dev/null +++ b/example-kernels/Cargo.toml @@ -0,0 +1,8 @@ +[workspace] +members = [ + "basic", + "runner", + "runner-doctest", + "runner-fail-reboot", + "runner-test", +] diff --git a/example-kernels/basic/.gitignore b/example-kernels/basic/.gitignore new file mode 100644 index 0000000..eccd7b4 --- /dev/null +++ b/example-kernels/basic/.gitignore @@ -0,0 +1,2 @@ +/target/ +**/*.rs.bk diff --git a/example-kernels/basic/Cargo.toml b/example-kernels/basic/Cargo.toml new file mode 100644 index 0000000..5a9a509 --- /dev/null +++ b/example-kernels/basic/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "basic" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2018" + +[dependencies] +bootloader = "0.9.7" +x86_64 = "0.14.1" diff --git a/example-kernels/basic/src/main.rs b/example-kernels/basic/src/main.rs new file mode 100644 index 0000000..0acc995 --- /dev/null +++ b/example-kernels/basic/src/main.rs @@ -0,0 +1,28 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use core::panic::PanicInfo; + +/// This function is called on panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +#[no_mangle] // don't mangle the name of this function +pub extern "C" fn _start() -> ! { + // this function is the entry point, since the linker looks for a function + // named `_start` by default + + // exit QEMU (see https://os.phil-opp.com/integration-tests/#shutting-down-qemu) + unsafe { exit_qemu(); } + + loop {} +} + +pub unsafe fn exit_qemu() { + use x86_64::instructions::port::Port; + + let mut port = Port::::new(0xf4); + port.write(51); // exit code is (51 << 1) | 1 = 103 +} diff --git a/example-kernels/runner-doctest/.cargo/config b/example-kernels/runner-doctest/.cargo/config new file mode 100644 index 0000000..3b4d89e --- /dev/null +++ b/example-kernels/runner-doctest/.cargo/config @@ -0,0 +1,5 @@ +[build] +target = "../x86_64-bootimage-example-kernels.json" + +[target.'cfg(target_os = "none")'] +runner = "bootimage runner" diff --git a/example-kernels/runner-doctest/.gitignore b/example-kernels/runner-doctest/.gitignore new file mode 100644 index 0000000..eccd7b4 --- /dev/null +++ b/example-kernels/runner-doctest/.gitignore @@ -0,0 +1,2 @@ +/target/ +**/*.rs.bk diff --git a/example-kernels/runner-doctest/Cargo.toml b/example-kernels/runner-doctest/Cargo.toml new file mode 100644 index 0000000..ff240ec --- /dev/null +++ b/example-kernels/runner-doctest/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "runner-doctest" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2018" + +[dependencies] +bootloader = "0.9.7" +x86_64 = "0.14.1" +rlibc = "1.0.0" + +[package.metadata.bootimage] +test-success-exit-code = 33 # (0x10 << 1) | 1 +test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-display", "none"] diff --git a/example-kernels/runner-doctest/src/lib.rs b/example-kernels/runner-doctest/src/lib.rs new file mode 100644 index 0000000..437f374 --- /dev/null +++ b/example-kernels/runner-doctest/src/lib.rs @@ -0,0 +1,92 @@ +#![no_std] +#![cfg_attr(test, no_main)] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +extern crate rlibc; + +/// add two numbers +/// +/// ``` +/// #![no_std] +/// #![no_main] +/// use runner_doctest::{add, exit_qemu, ExitCode}; +/// #[export_name = "_start"] +/// extern "C" fn start() { +/// assert_eq!(add(1, 2), 3); +/// unsafe { exit_qemu(ExitCode::Success); } +/// } +/// ``` +pub fn add(a: u32, b: u32) -> u32 { + a + b +} + +/// multiply two numbers +/// +/// ``` +/// #![no_std] +/// #![no_main] +/// use runner_doctest::{mul, exit_qemu, ExitCode}; +/// #[export_name = "_start"] +/// extern "C" fn start() { +/// assert_eq!(mul(2, 3), 6); +/// unsafe { exit_qemu(ExitCode::Success); } +/// } +/// ``` +pub fn mul(a: u32, b: u32) -> u32 { + a * b +} + +#[cfg(test)] +fn test_runner(tests: &[&dyn Fn()]) { + for test in tests.iter() { + test(); + } + + unsafe { + exit_qemu(ExitCode::Success); + } +} + +pub enum ExitCode { + Success, + Failed, +} + +impl ExitCode { + fn code(&self) -> u32 { + match self { + ExitCode::Success => 0x10, + ExitCode::Failed => 0x11, + } + } +} + +/// exit QEMU (see https://os.phil-opp.com/integration-tests/#shutting-down-qemu) +pub unsafe fn exit_qemu(exit_code: ExitCode) { + use x86_64::instructions::port::Port; + + let mut port = Port::::new(0xf4); + port.write(exit_code.code()); +} + +#[cfg(test)] +#[no_mangle] +pub extern "C" fn _start() -> ! { + test_main(); + + unsafe { + exit_qemu(ExitCode::Failed); + } + + loop {} +} + +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + exit_qemu(ExitCode::Failed); + } + loop {} +} diff --git a/example-kernels/runner-fail-reboot/.cargo/config b/example-kernels/runner-fail-reboot/.cargo/config new file mode 100644 index 0000000..3b4d89e --- /dev/null +++ b/example-kernels/runner-fail-reboot/.cargo/config @@ -0,0 +1,5 @@ +[build] +target = "../x86_64-bootimage-example-kernels.json" + +[target.'cfg(target_os = "none")'] +runner = "bootimage runner" diff --git a/example-kernels/runner-fail-reboot/.gitignore b/example-kernels/runner-fail-reboot/.gitignore new file mode 100644 index 0000000..eccd7b4 --- /dev/null +++ b/example-kernels/runner-fail-reboot/.gitignore @@ -0,0 +1,2 @@ +/target/ +**/*.rs.bk diff --git a/example-kernels/runner-fail-reboot/Cargo.toml b/example-kernels/runner-fail-reboot/Cargo.toml new file mode 100644 index 0000000..c15f396 --- /dev/null +++ b/example-kernels/runner-fail-reboot/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "runner-fail-reboot" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2018" + +[dependencies] +bootloader = "0.9.7" +x86_64 = "0.14.1" +rlibc = "1.0.0" + +[package.metadata.bootimage] +test-success-exit-code = 0 # this will test for the reboot +test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-display", "none"] diff --git a/example-kernels/runner-fail-reboot/src/lib.rs b/example-kernels/runner-fail-reboot/src/lib.rs new file mode 100644 index 0000000..3472c56 --- /dev/null +++ b/example-kernels/runner-fail-reboot/src/lib.rs @@ -0,0 +1,71 @@ +#![no_std] +#![cfg_attr(test, no_main)] +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +extern crate rlibc; + +pub fn test_runner(tests: &[&dyn Fn()]) { + for test in tests.iter() { + test(); + } + + unsafe { + exit_qemu(ExitCode::Success); + } +} + +#[test_case] +fn should_reboot() { + // this overflows the stack which leads to a triple fault + // the as-if rule might allow this to get optimized away on release builds + #[allow(unconditional_recursion)] + fn stack_overflow() { + stack_overflow() + } + stack_overflow() +} + +pub enum ExitCode { + Success, + Failed, +} + +impl ExitCode { + fn code(&self) -> u32 { + match self { + ExitCode::Success => 0x10, + ExitCode::Failed => 0x11, + } + } +} + +/// exit QEMU (see https://os.phil-opp.com/integration-tests/#shutting-down-qemu) +pub unsafe fn exit_qemu(exit_code: ExitCode) { + use x86_64::instructions::port::Port; + + let mut port = Port::::new(0xf4); + port.write(exit_code.code()); +} + +#[cfg(test)] +#[no_mangle] +pub extern "C" fn _start() -> ! { + test_main(); + + unsafe { + exit_qemu(ExitCode::Failed); + } + + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { + exit_qemu(ExitCode::Failed); + } + loop {} +} diff --git a/example-kernels/runner-test/.cargo/config b/example-kernels/runner-test/.cargo/config new file mode 100644 index 0000000..3b4d89e --- /dev/null +++ b/example-kernels/runner-test/.cargo/config @@ -0,0 +1,5 @@ +[build] +target = "../x86_64-bootimage-example-kernels.json" + +[target.'cfg(target_os = "none")'] +runner = "bootimage runner" diff --git a/example-kernels/runner-test/.gitignore b/example-kernels/runner-test/.gitignore new file mode 100644 index 0000000..eccd7b4 --- /dev/null +++ b/example-kernels/runner-test/.gitignore @@ -0,0 +1,2 @@ +/target/ +**/*.rs.bk diff --git a/example-kernels/runner-test/Cargo.toml b/example-kernels/runner-test/Cargo.toml new file mode 100644 index 0000000..83bcb52 --- /dev/null +++ b/example-kernels/runner-test/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "runner-test" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2018" + +[[test]] +name = "no-harness" +harness = false + +[dependencies] +bootloader = "0.9.7" +x86_64 = "0.14.1" +rlibc = "1.0.0" + +[package.metadata.bootimage] +test-success-exit-code = 33 # (0x10 << 1) | 1 +test-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-display", "none"] diff --git a/example-kernels/runner-test/src/lib.rs b/example-kernels/runner-test/src/lib.rs new file mode 100644 index 0000000..740e7a9 --- /dev/null +++ b/example-kernels/runner-test/src/lib.rs @@ -0,0 +1,65 @@ +#![no_std] +#![cfg_attr(test, no_main)] + +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +extern crate rlibc; + +pub fn test_runner(tests: &[&dyn Fn()]) { + for test in tests.iter() { + test(); + } + + unsafe { exit_qemu(ExitCode::Success); } +} + +#[test_case] +fn test1() { + assert_eq!(0, 0); +} + +#[test_case] +fn test2() { + assert_eq!(1, 1); +} + +pub enum ExitCode { + Success, + Failed +} + +impl ExitCode { + fn code(&self) -> u32 { + match self { + ExitCode::Success => 0x10, + ExitCode::Failed => 0x11, + } + } +} + +/// exit QEMU (see https://os.phil-opp.com/integration-tests/#shutting-down-qemu) +pub unsafe fn exit_qemu(exit_code: ExitCode) { + use x86_64::instructions::port::Port; + + let mut port = Port::::new(0xf4); + port.write(exit_code.code()); +} + +#[cfg(test)] +#[no_mangle] +pub extern "C" fn _start() -> ! { + test_main(); + + unsafe { exit_qemu(ExitCode::Failed); } + + loop {} +} + +#[cfg(test)] +#[panic_handler] +fn panic(_info: &core::panic::PanicInfo) -> ! { + unsafe { exit_qemu(ExitCode::Failed); } + loop {} +} diff --git a/example-kernels/runner-test/src/main.rs b/example-kernels/runner-test/src/main.rs new file mode 100644 index 0000000..67c5e90 --- /dev/null +++ b/example-kernels/runner-test/src/main.rs @@ -0,0 +1,30 @@ +#![no_std] +#![no_main] + +#![feature(custom_test_frameworks)] +#![test_runner(runner_test::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; +use runner_test::{exit_qemu, ExitCode}; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + #[cfg(test)] + test_main(); + + unsafe { exit_qemu(ExitCode::Failed); } + + loop {} +} + +#[test_case] +fn test1() { + assert_eq!(0, 0); +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + unsafe { exit_qemu(ExitCode::Failed); } + loop {} +} diff --git a/example-kernels/runner-test/tests/no-harness.rs b/example-kernels/runner-test/tests/no-harness.rs new file mode 100644 index 0000000..4c20516 --- /dev/null +++ b/example-kernels/runner-test/tests/no-harness.rs @@ -0,0 +1,21 @@ +#![no_std] +#![no_main] + +use runner_test::{exit_qemu, ExitCode}; +use core::panic::PanicInfo; + +#[no_mangle] +pub extern "C" fn _start() -> ! { + unsafe { + exit_qemu(ExitCode::Success); + } + loop {} +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + unsafe { + exit_qemu(ExitCode::Failed); + } + loop {} +} diff --git a/example-kernels/runner-test/tests/should-panic.rs b/example-kernels/runner-test/tests/should-panic.rs new file mode 100644 index 0000000..4dfb643 --- /dev/null +++ b/example-kernels/runner-test/tests/should-panic.rs @@ -0,0 +1,35 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +#![feature(custom_test_frameworks)] +#![test_runner(crate::test_runner)] +#![reexport_test_harness_main = "test_main"] + +use core::panic::PanicInfo; +use runner_test::{exit_qemu, ExitCode}; + +#[no_mangle] // don't mangle the name of this function +pub extern "C" fn _start() -> ! { + test_main(); + + unsafe { exit_qemu(ExitCode::Failed); } + + loop {} +} + +pub fn test_runner(tests: &[&dyn Fn()]) { + for test in tests.iter() { + test(); + } +} + +#[test_case] +fn should_panic() { + assert_eq!(1, 2); +} + +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + unsafe { exit_qemu(ExitCode::Success); } + loop {} +} diff --git a/example-kernels/runner/.cargo/config b/example-kernels/runner/.cargo/config new file mode 100644 index 0000000..3b4d89e --- /dev/null +++ b/example-kernels/runner/.cargo/config @@ -0,0 +1,5 @@ +[build] +target = "../x86_64-bootimage-example-kernels.json" + +[target.'cfg(target_os = "none")'] +runner = "bootimage runner" diff --git a/example-kernels/runner/.gitignore b/example-kernels/runner/.gitignore new file mode 100644 index 0000000..eccd7b4 --- /dev/null +++ b/example-kernels/runner/.gitignore @@ -0,0 +1,2 @@ +/target/ +**/*.rs.bk diff --git a/example-kernels/runner/Cargo.toml b/example-kernels/runner/Cargo.toml new file mode 100644 index 0000000..17d0be9 --- /dev/null +++ b/example-kernels/runner/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "runner" +version = "0.1.0" +authors = ["Philipp Oppermann "] +edition = "2018" + +[dependencies] +bootloader = "0.9.7" +x86_64 = "0.14.1" + +[package.metadata.bootimage] +run-args = ["-device", "isa-debug-exit,iobase=0xf4,iosize=0x04", "-display", "none"] diff --git a/example-kernels/runner/src/main.rs b/example-kernels/runner/src/main.rs new file mode 100644 index 0000000..033f446 --- /dev/null +++ b/example-kernels/runner/src/main.rs @@ -0,0 +1,28 @@ +#![no_std] // don't link the Rust standard library +#![no_main] // disable all Rust-level entry points + +use core::panic::PanicInfo; + +/// This function is called on panic. +#[panic_handler] +fn panic(_info: &PanicInfo) -> ! { + loop {} +} + +#[no_mangle] // don't mangle the name of this function +pub extern "C" fn _start() -> ! { + // this function is the entry point, since the linker looks for a function + // named `_start` by default + + // exit QEMU (see https://os.phil-opp.com/integration-tests/#shutting-down-qemu) + unsafe { exit_qemu(); } + + loop {} +} + +pub unsafe fn exit_qemu() { + use x86_64::instructions::port::Port; + + let mut port = Port::::new(0xf4); + port.write(54); // exit code is (54 << 1) | 1 = 109 +} diff --git a/example-kernels/rust-toolchain b/example-kernels/rust-toolchain new file mode 100644 index 0000000..07ade69 --- /dev/null +++ b/example-kernels/rust-toolchain @@ -0,0 +1 @@ +nightly \ No newline at end of file diff --git a/example-kernels/x86_64-bootimage-example-kernels.json b/example-kernels/x86_64-bootimage-example-kernels.json new file mode 100644 index 0000000..b982009 --- /dev/null +++ b/example-kernels/x86_64-bootimage-example-kernels.json @@ -0,0 +1,16 @@ +{ + "llvm-target": "x86_64-unknown-none", + "data-layout": "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-i128:128-f80:128-n8:16:32:64-S128", + "arch": "x86_64", + "target-endian": "little", + "target-pointer-width": "64", + "target-c-int-width": 32, + "os": "none", + "executables": true, + "linker-flavor": "ld.lld", + "linker": "rust-lld", + "panic-strategy": "abort", + "disable-redzone": true, + "features": "-mmx,-sse,+soft-float", + "rustc-abi": "x86-softfloat" + } diff --git a/src/args.rs b/src/args.rs deleted file mode 100644 index 7d5c8ef..0000000 --- a/src/args.rs +++ /dev/null @@ -1,186 +0,0 @@ -use std::{env, mem}; -use std::path::PathBuf; -use Command; - -pub(crate) fn parse_args() -> Command { - let mut args = env::args().skip(1); - let first = args.next(); - match first.as_ref().map(|s| s.as_str()) { - Some("build") => parse_build_args(args), - Some("run") => match parse_build_args(args) { - Command::Build(args) => Command::Run(args), - Command::BuildHelp => Command::RunHelp, - cmd => cmd, - }, - Some("test") => match parse_build_args(args) { - Command::Build(args) => { - assert_eq!(args.bin_name, None, "No `--bin` argument allowed for `bootimage test`"); - Command::Test(args) - }, - Command::BuildHelp => Command::TestHelp, - cmd => cmd, - }, - Some("--help") | Some("-h") => Command::Help, - Some("--version") => Command::Version, - _ => Command::NoSubcommand, - } -} - -fn parse_build_args(args: A) -> Command -where - A: Iterator, -{ - let mut manifest_path: Option = None; - let mut bin_name: Option = None; - let mut target: Option = None; - let mut release: Option = None; - let mut update_bootloader: Option = None; - let mut cargo_args = Vec::new(); - let mut run_args = Vec::new(); - let mut run_args_started = false; - { - fn set(arg: &mut Option, value: Option) { - let previous = mem::replace(arg, value); - assert!( - previous.is_none(), - "multiple arguments of same type provided" - ) - }; - - let mut arg_iter = args.into_iter(); - while let Some(arg) = arg_iter.next() { - if run_args_started { - run_args.push(arg); - continue; - } - match arg.as_ref() { - "--help" | "-h" => { - return Command::BuildHelp; - } - "--version" => { - return Command::Version; - } - "--bin" => { - let next = arg_iter.next(); - set(&mut bin_name, next.clone()); - cargo_args.push(arg); - if let Some(next) = next { - cargo_args.push(next); - } - } - _ if arg.starts_with("--bin=") => { - set( - &mut bin_name, - Some(String::from(arg.trim_left_matches("--bin="))), - ); - cargo_args.push(arg); - } - "--target" => { - let next = arg_iter.next(); - set(&mut target, next.clone()); - cargo_args.push(arg); - if let Some(next) = next { - cargo_args.push(next); - } - } - _ if arg.starts_with("--target=") => { - set( - &mut target, - Some(String::from(arg.trim_left_matches("--target="))), - ); - cargo_args.push(arg); - } - "--manifest-path" => { - let next = arg_iter.next(); - set(&mut manifest_path, next.as_ref().map(|p| PathBuf::from(&p))); - cargo_args.push(arg); - if let Some(next) = next { - cargo_args.push(next); - } - } - _ if arg.starts_with("--manifest-path=") => { - let path = PathBuf::from(arg.trim_left_matches("--manifest-path=")); - set(&mut manifest_path, Some(path)); - cargo_args.push(arg); - } - "--release" => { - set(&mut release, Some(true)); - cargo_args.push(arg); - } - "--update-bootloader" => { - set(&mut update_bootloader, Some(true)); - } - "--" => { - run_args_started = true; - } - _ => { - cargo_args.push(arg); - } - }; - } - } - - Command::Build(Args { - cargo_args, - run_args, - bin_name, - target, - manifest_path, - release: release.unwrap_or(false), - update_bootloader: update_bootloader.unwrap_or(false), - }) -} - -#[derive(Debug, Clone)] -pub struct Args { - /// All arguments that are passed to cargo. - pub cargo_args: Vec, - /// All arguments that are passed to the runner. - pub run_args: Vec, - /// The manifest path (also present in `cargo_args`). - manifest_path: Option, - /// The name of the binary (passed `--bin` argument) (also present in `cargo_args`). - bin_name: Option, - /// The target triple (also present in `cargo_args`). - target: Option, - /// The release flag (also present in `cargo_args`). - release: bool, - /// Whether the bootloader should be updated (not present in `cargo_args`). - update_bootloader: bool, -} - -impl Args { - pub fn manifest_path(&self) -> &Option { - &self.manifest_path - } - - pub fn bin_name(&self) -> &Option { - &self.bin_name - } - - pub fn target(&self) -> &Option { - &self.target - } - - pub fn release(&self) -> bool { - self.release - } - - pub fn update_bootloader(&self) -> bool { - self.update_bootloader - } - - pub fn set_target(&mut self, target: String) { - assert!(self.target.is_none()); - self.target = Some(target.clone()); - self.cargo_args.push("--target".into()); - self.cargo_args.push(target); - } - - pub fn set_bin_name(&mut self, bin_name: String) { - assert!(self.bin_name.is_none()); - self.bin_name = Some(bin_name.clone()); - self.cargo_args.push("--bin".into()); - self.cargo_args.push(bin_name); - } -} diff --git a/src/args/build.rs b/src/args/build.rs new file mode 100644 index 0000000..c3d18a9 --- /dev/null +++ b/src/args/build.rs @@ -0,0 +1,109 @@ +use anyhow::{anyhow, Context, Result}; +use std::{ + mem, + path::{Path, PathBuf}, +}; + +/// Internal representation of the `cargo bootimage` command. +pub enum BuildCommand { + /// A normal invocation (i.e. no `--help` or `--version`) + Build(BuildArgs), + /// The `--version` command + Version, + /// The `--help` command + Help, +} + +impl BuildCommand { + /// Parse the command line args into a `BuildCommand`. + pub fn parse_args(args: A) -> Result + where + A: Iterator, + { + let mut manifest_path: Option = None; + let mut cargo_args = Vec::new(); + let mut quiet = false; + { + fn set(arg: &mut Option, value: Option) -> Result<()> { + let previous = mem::replace(arg, value); + if previous.is_some() { + return Err(anyhow!("multiple arguments of same type provided")); + } + Ok(()) + } + + let mut arg_iter = args; + while let Some(arg) = arg_iter.next() { + match arg.as_ref() { + "--help" | "-h" => { + return Ok(BuildCommand::Help); + } + "--version" => { + return Ok(BuildCommand::Version); + } + "--quiet" => { + quiet = true; + } + "--manifest-path" => { + let next = arg_iter.next(); + set( + &mut manifest_path, + next.as_ref() + .map(|p| Path::new(&p).canonicalize()) + .transpose() + .context("--manifest-path invalid")?, + )?; + cargo_args.push(arg); + if let Some(next) = next { + cargo_args.push(next); + } + } + _ if arg.starts_with("--manifest-path=") => { + let path = Path::new(arg.trim_start_matches("--manifest-path=")) + .canonicalize() + .context("--manifest-path invalid")?; + set(&mut manifest_path, Some(path))?; + cargo_args.push(arg); + } + _ => { + cargo_args.push(arg); + } + }; + } + } + + Ok(BuildCommand::Build(BuildArgs { + manifest_path, + cargo_args, + quiet, + })) + } +} + +/// Arguments passed to `cargo bootimage`. +#[derive(Debug, Clone)] +pub struct BuildArgs { + /// The manifest path (also present in `cargo_args`). + manifest_path: Option, + /// All arguments that are passed to cargo. + cargo_args: Vec, + /// Suppress any output to stdout. + quiet: bool, +} + +impl BuildArgs { + /// The value of the `--manifest-path` argument, if any. + pub fn manifest_path(&self) -> Option<&Path> { + self.manifest_path.as_deref() + } + + /// Arguments that should be forwarded to `cargo build`. + pub fn cargo_args(&self) -> &[String] { + &self.cargo_args.as_ref() + } + + /// Whether a `--quiet` flag was passed. + pub fn quiet(&self) -> bool { + self.quiet + } +} diff --git a/src/args/mod.rs b/src/args/mod.rs new file mode 100644 index 0000000..e491c92 --- /dev/null +++ b/src/args/mod.rs @@ -0,0 +1,7 @@ +//! Parses command line arguments. + +pub use build::*; +pub use runner::*; + +mod build; +mod runner; diff --git a/src/args/runner.rs b/src/args/runner.rs new file mode 100644 index 0000000..bfaf2ed --- /dev/null +++ b/src/args/runner.rs @@ -0,0 +1,72 @@ +use anyhow::{anyhow, Result}; +use std::path::PathBuf; + +/// Internal representation of the `bootimage runner` command. +pub enum RunnerCommand { + /// A normal invocation of `bootimage runner` (i.e. no `--help` or `--version`) + Runner(RunnerArgs), + /// A command containing `--version` + Version, + /// A command containing `--help` + Help, +} + +impl RunnerCommand { + /// Parse the given argument set into the internal representation. + pub fn parse_args(args: A) -> Result + where + A: Iterator, + { + let mut executable = None; + let mut quiet = false; + let mut runner_args = None; + + let mut arg_iter = args.fuse(); + + loop { + if executable.is_some() { + let args: Vec<_> = arg_iter.collect(); + if !args.is_empty() { + runner_args = Some(args); + } + break; + } + let next = match arg_iter.next() { + Some(next) => next, + None => break, + }; + match next.as_str() { + "--help" | "-h" => { + return Ok(RunnerCommand::Help); + } + "--version" => { + return Ok(RunnerCommand::Version); + } + "--quiet" => { + quiet = true; + } + exe => { + executable = Some(PathBuf::from(exe)); + } + } + } + + Ok(Self::Runner(RunnerArgs { + executable: executable + .ok_or_else(|| anyhow!("excepted path to kernel executable as first argument"))?, + quiet, + runner_args, + })) + } +} + +/// Arguments for the `bootimage runner` command +#[derive(Debug, Clone)] +pub struct RunnerArgs { + /// Path to the executable binary + pub executable: PathBuf, + /// Suppress any output to stdout. + pub quiet: bool, + /// Additional arguments passed to the runner + pub runner_args: Option>, +} diff --git a/src/bin/cargo-bootimage.rs b/src/bin/cargo-bootimage.rs new file mode 100644 index 0000000..3ea1690 --- /dev/null +++ b/src/bin/cargo-bootimage.rs @@ -0,0 +1,91 @@ +use anyhow::{anyhow, Context, Result}; +use bootimage::{ + args::{BuildArgs, BuildCommand}, + builder::Builder, + config, help, +}; +use std::{ + env, + path::{Path, PathBuf}, +}; + +pub fn main() -> Result<()> { + let mut raw_args = env::args(); + + let executable_name = raw_args + .next() + .ok_or_else(|| anyhow!("no first argument (executable name)"))?; + let file_stem = Path::new(&executable_name) + .file_stem() + .and_then(|s| s.to_str()); + if file_stem != Some("cargo-bootimage") { + return Err(anyhow!( + "Unexpected executable name: expected `cargo-bootimage`, got: `{:?}`", + file_stem + )); + } + if raw_args.next().as_deref() != Some("bootimage") { + return Err(anyhow!("Please invoke this as `cargo bootimage`")); + } + + match BuildCommand::parse_args(raw_args)? { + BuildCommand::Build(args) => build(args), + BuildCommand::Version => { + help::print_version(); + Ok(()) + } + BuildCommand::Help => { + help::print_cargo_bootimage_help(); + Ok(()) + } + } +} + +fn build(args: BuildArgs) -> Result<()> { + let mut builder = Builder::new(args.manifest_path().map(PathBuf::from))?; + let config = config::read_config(builder.manifest_path())?; + let quiet = args.quiet(); + + let executables = builder.build_kernel(&args.cargo_args(), &config, quiet)?; + if executables.is_empty() { + return Err(anyhow!("no executables built")); + } + + for executable in executables { + let out_dir = executable + .parent() + .ok_or_else(|| anyhow!("executable has no parent path"))?; + let bin_name = &executable + .file_stem() + .ok_or_else(|| anyhow!("executable has no file stem"))? + .to_str() + .ok_or_else(|| anyhow!("executable file stem not valid utf8"))?; + + // We don't have access to a CARGO_MANIFEST_DIR environment variable + // here because `cargo bootimage` is started directly by the user. We + // therefore have to find out the path to the Cargo.toml of the + // executables ourselves. For workspace projects, this can be a + // different Cargo.toml than the Cargo.toml in the current directory. + // + // To retrieve the correct Cargo.toml path, we look for the binary name + // in the `cargo metadata` output and then get the manifest path from + // the corresponding package. + let kernel_package = builder + .kernel_package_for_bin(bin_name) + .context("Failed to run cargo metadata to find out kernel manifest path")? + .ok_or_else(|| anyhow!("Failed to find kernel binary in cargo metadata output"))?; + let kernel_manifest_path = &kernel_package.manifest_path.to_owned(); + + let bootimage_path = out_dir.join(format!("bootimage-{}.bin", bin_name)); + builder.create_bootimage(kernel_manifest_path, &executable, &bootimage_path, quiet)?; + if !args.quiet() { + println!( + "Created bootimage for `{}` at `{}`", + bin_name, + bootimage_path.display() + ); + } + } + + Ok(()) +} diff --git a/src/build.rs b/src/build.rs deleted file mode 100644 index c0e40a1..0000000 --- a/src/build.rs +++ /dev/null @@ -1,393 +0,0 @@ -use std::fs::{self, File}; -use std::{io, process}; -use std::path::{Path, PathBuf}; -use byteorder::{ByteOrder, LittleEndian}; -use args::{self, Args}; -use config::{self, Config}; -use cargo_metadata::{self, Metadata as CargoMetadata, Package as CrateMetadata}; -use Error; -use xmas_elf; -use tempdir::TempDir; - -const BLOCK_SIZE: usize = 512; -type KernelInfoBlock = [u8; BLOCK_SIZE]; - -pub(crate) fn build(args: Args) -> Result<(), Error> { - let (args, config, metadata, root_dir, out_dir) = common_setup(args)?; - - build_impl(&args, &config, &metadata, &root_dir, &out_dir, true)?; - Ok(()) -} - -pub(crate) fn run(args: Args) -> Result<(), Error> { - let (args, config, metadata, root_dir, out_dir) = common_setup(args)?; - - let output_path = build_impl(&args, &config, &metadata, &root_dir, &out_dir, true)?; - run_impl(&args, &config, &output_path) -} - -pub(crate) fn common_setup(mut args: Args) -> Result<(Args, Config, CargoMetadata, PathBuf, PathBuf), Error> { - fn out_dir(args: &Args, metadata: &CargoMetadata) -> PathBuf { - let target_dir = PathBuf::from(&metadata.target_directory); - let mut out_dir = target_dir; - if let &Some(ref target) = args.target() { - out_dir.push(Path::new(target).file_stem().unwrap().to_str().unwrap()); - } - if args.release() { - out_dir.push("release"); - } else { - out_dir.push("debug"); - } - out_dir - } - - let metadata = read_cargo_metadata(&args)?; - let crate_root = PathBuf::from(&metadata.workspace_root); - let manifest_path = args.manifest_path().as_ref().map(Clone::clone).unwrap_or({ - let mut path = crate_root.clone(); - path.push("Cargo.toml"); - path - }); - let config = config::read_config(manifest_path)?; - - if args.target().is_none() { - if let Some(ref target) = config.default_target { - args.set_target(target.clone()); - } - } - - if let &Some(ref target) = args.target() { - if !target.ends_with(".json") { - use std::io::{self, Write}; - use std::process; - - writeln!( - io::stderr(), - "Please pass a path to `--target` (with `.json` extension`): `--target {}.json`", - target - ).unwrap(); - process::exit(1); - } - } - - let out_dir = out_dir(&args, &metadata); - - Ok((args, config, metadata, crate_root, out_dir)) -} - -pub(crate) fn build_impl( - args: &Args, - config: &Config, - metadata: &CargoMetadata, - root_dir: &Path, - out_dir: &Path, - verbose: bool, -) -> Result { - let crate_ = metadata - .packages - .iter() - .find(|p| Path::new(&p.manifest_path) == config.manifest_path) - .expect("Could not read crate name from cargo metadata"); - let bin_name: String = args.bin_name().as_ref().unwrap_or(&crate_.name).clone(); - - let kernel = build_kernel(&out_dir, &bin_name, &args, verbose)?; - - let kernel_size = kernel.metadata()?.len(); - let kernel_info_block = create_kernel_info_block(kernel_size); - - if args.update_bootloader() { - let mut bootloader_cargo_lock = PathBuf::from(out_dir); - bootloader_cargo_lock.push("bootloader"); - bootloader_cargo_lock.push("Cargo.lock"); - - fs::remove_file(bootloader_cargo_lock)?; - } - - let tmp_dir = TempDir::new("bootloader")?; - let bootloader = build_bootloader(tmp_dir.path(), &config)?; - tmp_dir.close()?; - - create_disk_image(root_dir, out_dir, &bin_name, &config, kernel, kernel_info_block, &bootloader, verbose) -} - -fn run_impl(args: &Args, config: &Config, output_path: &Path) -> Result<(), Error> { - let command = &config.run_command[0]; - let mut command = process::Command::new(command); - for arg in &config.run_command[1..] { - command.arg( - arg.replace( - "{}", - output_path - .to_str() - .expect("output must be valid unicode"), - ), - ); - } - command.args(&args.run_args); - command.status()?; - Ok(()) -} - -fn read_cargo_metadata(args: &Args) -> Result { - cargo_metadata::metadata(args.manifest_path().as_ref().map(PathBuf::as_path)) -} - -fn build_kernel( - out_dir: &Path, - bin_name: &str, - args: &args::Args, - verbose: bool, -) -> Result { - // compile kernel - if verbose { - println!("Building kernel"); - } - let exit_status = run_xbuild(&args.cargo_args)?; - if !exit_status.success() { - process::exit(1) - } - - let mut kernel_path = out_dir.to_owned(); - kernel_path.push(bin_name); - let kernel = File::open(kernel_path)?; - Ok(kernel) -} - -fn run_xbuild(args: &[String]) -> io::Result { - let mut command = process::Command::new("cargo"); - command.arg("xbuild"); - command.args(args); - command.status() -} - -fn create_kernel_info_block(kernel_size: u64) -> KernelInfoBlock { - let kernel_size = if kernel_size <= u64::from(u32::max_value()) { - kernel_size as u32 - } else { - panic!("Kernel can't be loaded by BIOS bootloader because is too big") - }; - - let mut kernel_info_block = [0u8; BLOCK_SIZE]; - LittleEndian::write_u32(&mut kernel_info_block[0..4], kernel_size); - - kernel_info_block -} - -fn download_bootloader(bootloader_dir: &Path, config: &Config) -> Result { - use std::io::Write; - - let cargo_toml = { - let mut dir = bootloader_dir.to_owned(); - dir.push("Cargo.toml"); - dir - }; - let src_lib = { - let mut dir = bootloader_dir.to_owned(); - dir.push("src"); - fs::create_dir_all(dir.as_path())?; - dir.push("lib.rs"); - dir - }; - - { - let mut cargo_toml_file = File::create(&cargo_toml)?; - cargo_toml_file.write_all( - r#" - [package] - authors = ["author@example.com>"] - name = "bootloader_download_helper" - version = "0.0.0" - - "#.as_bytes(), - )?; - cargo_toml_file.write_all( - format!( - r#" - [dependencies.{}] - "#, - config.bootloader.name - ).as_bytes(), - )?; - if let &Some(ref version) = &config.bootloader.version { - cargo_toml_file.write_all( - format!( - r#" - version = "{}" - "#, - version - ).as_bytes(), - )?; - } - if let &Some(ref git) = &config.bootloader.git { - cargo_toml_file.write_all( - format!( - r#" - git = "{}" - "#, - git - ).as_bytes(), - )?; - } - if let &Some(ref branch) = &config.bootloader.branch { - cargo_toml_file.write_all( - format!( - r#" - branch = "{}" - "#, - branch - ).as_bytes(), - )?; - } - if let &Some(ref path) = &config.bootloader.path { - cargo_toml_file.write_all( - format!( - r#" - path = "{}" - "#, - path.display() - ).as_bytes(), - )?; - } - - File::create(src_lib)?.write_all( - r#" - #![no_std] - "#.as_bytes(), - )?; - } - - let mut command = process::Command::new("cargo"); - command.arg("fetch"); - command.current_dir(bootloader_dir); - assert!(command.status()?.success(), "Bootloader download failed."); - - let metadata = cargo_metadata::metadata_deps(Some(&cargo_toml), true)?; - let bootloader = metadata - .packages - .iter() - .find(|p| p.name == config.bootloader.name) - .expect(&format!( - "Could not find crate named “{}”", - config.bootloader.name - )); - - Ok(bootloader.clone()) -} - -fn build_bootloader(bootloader_dir: &Path, config: &Config) -> Result, Error> { - use std::io::Read; - - let bootloader_metadata = download_bootloader(bootloader_dir, config)?; - let bootloader_dir = Path::new(&bootloader_metadata.manifest_path) - .parent() - .unwrap(); - - let mut bootloader_target_path = PathBuf::from(bootloader_dir); - bootloader_target_path.push(&config.bootloader.target); - - let bootloader_elf_path = if !config.bootloader.precompiled { - let args = &[ - String::from("--manifest-path"), - bootloader_metadata.manifest_path.clone(), - String::from("--target"), - bootloader_target_path.display().to_string(), - String::from("--release"), - ]; - - println!("Building bootloader"); - let exit_status = run_xbuild(args)?; - if !exit_status.success() { - process::exit(1) - } - - let mut bootloader_elf_path = bootloader_dir.to_path_buf(); - bootloader_elf_path.push("target"); - bootloader_elf_path.push(config.bootloader.target.file_stem().unwrap()); - bootloader_elf_path.push("release"); - bootloader_elf_path.push("bootloader"); - bootloader_elf_path - } else { - let mut bootloader_elf_path = bootloader_dir.to_path_buf(); - bootloader_elf_path.push("bootloader"); - bootloader_elf_path - }; - - let mut bootloader_elf_bytes = Vec::new(); - let mut bootloader = File::open(&bootloader_elf_path).map_err(|err| { - Error::Bootloader( - format!( - "Could not open bootloader at {}", - bootloader_elf_path.display() - ), - err, - ) - })?; - bootloader.read_to_end(&mut bootloader_elf_bytes)?; - - // copy bootloader section of ELF file to bootloader_path - let elf_file = xmas_elf::ElfFile::new(&bootloader_elf_bytes).unwrap(); - xmas_elf::header::sanity_check(&elf_file).unwrap(); - let bootloader_section = elf_file - .find_section_by_name(".bootloader") - .expect("bootloader must have a .bootloader section"); - - Ok(Vec::from(bootloader_section.raw_data(&elf_file)).into_boxed_slice()) -} - -fn create_disk_image( - root_dir: &Path, - out_dir: &Path, - bin_name: &str, - config: &Config, - mut kernel: File, - kernel_info_block: KernelInfoBlock, - bootloader_data: &[u8], - verbose: bool, -) -> Result { - use std::io::{Read, Write}; - - let mut output_path = PathBuf::from(out_dir); - let file_name = format!("bootimage-{}.bin", bin_name); - output_path.push(file_name); - - if let Some(ref output) = config.output { - output_path = output.clone(); - } - - if verbose { - println!("Creating disk image at {}", - output_path.strip_prefix(root_dir).unwrap_or(output_path.as_path()).display()); - } - let mut output = File::create(&output_path)?; - output.write_all(&bootloader_data)?; - output.write_all(&kernel_info_block)?; - - // write out kernel elf file - let kernel_size = kernel.metadata()?.len(); - let mut buffer = [0u8; 1024]; - loop { - let (n, interrupted) = match kernel.read(&mut buffer) { - Ok(0) => break, - Ok(n) => (n, false), - Err(ref e) if e.kind() == io::ErrorKind::Interrupted => (0, true), - Err(e) => Err(e)?, - }; - if !interrupted { - output.write_all(&buffer[..n])? - } - } - - let padding_size = ((512 - (kernel_size % 512)) % 512) as usize; - let padding = [0u8; 512]; - output.write_all(&padding[..padding_size])?; - - if let Some(min_size) = config.minimum_image_size { - // we already wrote to output successfully, - // both metadata and set_len should succeed. - if output.metadata()?.len() < min_size { - output.set_len(min_size)?; - } - } - - Ok(output_path) -} diff --git a/src/builder/bootloader.rs b/src/builder/bootloader.rs new file mode 100644 index 0000000..5753fc1 --- /dev/null +++ b/src/builder/bootloader.rs @@ -0,0 +1,159 @@ +use super::error::BootloaderError; +use cargo_metadata::{Metadata, Package}; +use std::{ + fs, + path::{Path, PathBuf}, + process::Command, +}; + +pub struct BuildConfig { + manifest_path: PathBuf, + bootloader_name: String, + target: PathBuf, + features: Vec, + target_dir: PathBuf, + kernel_bin_path: PathBuf, + kernel_manifest_path: PathBuf, + build_std: Option, +} + +impl BuildConfig { + /// Derives the bootloader build config from the project's metadata. + pub fn from_metadata( + project_metadata: &Metadata, + kernel_manifest_path: &Path, + kernel_bin_path: &Path, + ) -> Result { + let kernel_pkg = project_metadata + .packages + .iter() + .find(|p| p.manifest_path == kernel_manifest_path) + .ok_or_else(|| BootloaderError::KernelPackageNotFound { + manifest_path: kernel_manifest_path.to_owned(), + })?; + + let bootloader_pkg = bootloader_package(project_metadata, kernel_pkg)?; + let bootloader_root = bootloader_pkg.manifest_path.parent().ok_or_else(|| { + BootloaderError::BootloaderInvalid("bootloader manifest has no target directory".into()) + })?; + + let cargo_toml_content = fs::read_to_string(&bootloader_pkg.manifest_path) + .map_err(|err| format!("bootloader has no valid Cargo.toml: {}", err)) + .map_err(BootloaderError::BootloaderInvalid)?; + let cargo_toml = cargo_toml_content + .parse::() + .map_err(|e| format!("Failed to parse Cargo.toml of bootloader: {}", e)) + .map_err(BootloaderError::BootloaderInvalid)?; + let metadata = cargo_toml.get("package").and_then(|t| t.get("metadata")); + let target = metadata + .and_then(|t| t.get("bootloader")) + .and_then(|t| t.get("target")); + let target_str = target.and_then(|v| v.as_str()).ok_or_else(|| { + BootloaderError::BootloaderInvalid( + "No `package.metadata.bootloader.target` key found in Cargo.toml of bootloader\n\n\ + (If you're using the official bootloader crate, you need at least version 0.5.1)" + .into(), + ) + })?; + let build_std = { + let key = metadata + .and_then(|t| t.get("bootloader")) + .and_then(|t| t.get("build-std")); + if let Some(key) = key { + let err_msg = "A non-string `package.metadata.bootloader.build-std` key found in \ + Cargo.toml of bootloader"; + let err = || BootloaderError::BootloaderInvalid(err_msg.into()); + Some(key.as_str().ok_or_else(err)?.into()) + } else { + None + } + }; + + let binary_feature = cargo_toml + .get("features") + .and_then(|f| f.get("binary")) + .is_some(); + + let resolve_opt = project_metadata.resolve.as_ref(); + let resolve = resolve_opt.ok_or(BootloaderError::CargoMetadataIncomplete { + key: "resolve".into(), + })?; + let bootloader_resolve = resolve + .nodes + .iter() + .find(|n| n.id == bootloader_pkg.id) + .ok_or(BootloaderError::CargoMetadataIncomplete { + key: format!("resolve[\"{}\"]", bootloader_pkg.name), + })?; + let mut features = bootloader_resolve.features.clone(); + if binary_feature { + features.push("binary".into()); + } + + let bootloader_name = &bootloader_pkg.name; + let target_dir = project_metadata + .target_directory + .join("bootimage") + .join(bootloader_name); + + Ok(BuildConfig { + manifest_path: bootloader_pkg.manifest_path.clone(), + target: bootloader_root.join(target_str), + features, + bootloader_name: bootloader_name.clone(), + target_dir, + kernel_manifest_path: kernel_pkg.manifest_path.clone(), + kernel_bin_path: kernel_bin_path.to_owned(), + build_std, + }) + } + + /// Creates the cargo build command for building the bootloader. + pub fn build_command(&self) -> Command { + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); + let mut cmd = Command::new(&cargo); + if let Some(build_std) = &self.build_std { + cmd.arg("build").arg(&format!("-Zbuild-std={}", build_std)); + } else { + cmd.arg("xbuild"); + } + cmd.arg("--manifest-path"); + cmd.arg(&self.manifest_path); + cmd.arg("--bin").arg(&self.bootloader_name); + cmd.arg("--target-dir").arg(&self.target_dir); + cmd.arg("--features") + .arg(self.features.as_slice().join(" ")); + cmd.arg("--target").arg(&self.target); + cmd.arg("--release"); + cmd.env("KERNEL", &self.kernel_bin_path); + cmd.env("KERNEL_MANIFEST", &self.kernel_manifest_path); + cmd.env("RUSTFLAGS", ""); + cmd.env( + "XBUILD_SYSROOT_PATH", + self.target_dir.join("bootloader-sysroot"), + ); // for cargo-xbuild + cmd + } +} + +/// Returns the package metadata for the bootloader crate +fn bootloader_package<'a>( + project_metadata: &'a Metadata, + kernel_package: &Package, +) -> Result<&'a Package, BootloaderError> { + let bootloader_name = { + let mut dependencies = kernel_package.dependencies.iter(); + let bootloader_package = dependencies + .find(|p| p.rename.as_ref().unwrap_or(&p.name) == "bootloader") + .ok_or(BootloaderError::BootloaderNotFound)?; + bootloader_package.name.clone() + }; + + project_metadata + .packages + .iter() + .find(|p| p.name == bootloader_name) + .ok_or(BootloaderError::CargoMetadataIncomplete { + key: format!("packages[name = `{}`", &bootloader_name), + }) +} diff --git a/src/builder/disk_image.rs b/src/builder/disk_image.rs new file mode 100644 index 0000000..2f3cfab --- /dev/null +++ b/src/builder/disk_image.rs @@ -0,0 +1,62 @@ +use super::error::DiskImageError; +use std::{path::Path, process::Command}; + +pub fn create_disk_image( + bootloader_elf_path: &Path, + output_bin_path: &Path, +) -> Result<(), DiskImageError> { + let llvm_tools = llvm_tools::LlvmTools::new()?; + let objcopy = llvm_tools + .tool(&llvm_tools::exe("llvm-objcopy")) + .ok_or(DiskImageError::LlvmObjcopyNotFound)?; + + // convert bootloader to binary + let mut cmd = Command::new(objcopy); + cmd.arg("-I").arg("elf64-x86-64"); + cmd.arg("-O").arg("binary"); + cmd.arg("--binary-architecture=i386:x86-64"); + cmd.arg(bootloader_elf_path); + cmd.arg(output_bin_path); + let output = cmd.output().map_err(|err| DiskImageError::Io { + message: "failed to execute llvm-objcopy command", + error: err, + })?; + if !output.status.success() { + return Err(DiskImageError::ObjcopyFailed { + stderr: output.stderr, + }); + } + + pad_to_nearest_block_size(output_bin_path)?; + Ok(()) +} + +fn pad_to_nearest_block_size(output_bin_path: &Path) -> Result<(), DiskImageError> { + const BLOCK_SIZE: u64 = 512; + use std::fs::OpenOptions; + let file = OpenOptions::new() + .write(true) + .open(&output_bin_path) + .map_err(|err| DiskImageError::Io { + message: "failed to open boot image", + error: err, + })?; + let file_size = file + .metadata() + .map_err(|err| DiskImageError::Io { + message: "failed to get size of boot image", + error: err, + })? + .len(); + let remainder = file_size % BLOCK_SIZE; + let padding = if remainder > 0 { + BLOCK_SIZE - remainder + } else { + 0 + }; + file.set_len(file_size + padding) + .map_err(|err| DiskImageError::Io { + message: "failed to pad boot image to a multiple of the block size", + error: err, + }) +} diff --git a/src/builder/error.rs b/src/builder/error.rs new file mode 100644 index 0000000..5d091fb --- /dev/null +++ b/src/builder/error.rs @@ -0,0 +1,160 @@ +use std::{io, path::PathBuf}; +use thiserror::Error; + +/// Represents an error that occurred while creating a new `Builder`. +#[derive(Debug, Error)] +pub enum BuilderError { + /// Failed to locate cargo manifest + #[error("Could not find Cargo.toml file starting from current folder: {0:?}")] + LocateCargoManifest(#[from] locate_cargo_manifest::LocateManifestError), +} + +/// Represents an error that occurred when building the kernel. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum BuildKernelError { + /// An unexpected I/O error occurred + #[error("I/O error: {message}:\n{error}")] + Io { + /// Desciption of the failed I/O operation + message: &'static str, + /// The I/O error that occurred + error: io::Error, + }, + + /// Could not find the `cargo xbuild` tool. Perhaps it is not installed? + #[error( + "Failed to run `cargo xbuild`. Perhaps it is not installed?\n\ + Run `cargo install cargo-xbuild` to install it." + )] + XbuildNotFound, + + /// Running `cargo build` failed. + #[error("Kernel build failed.\nStderr: {}", String::from_utf8_lossy(.stderr))] + BuildFailed { + /// The standard error output. + stderr: Vec, + }, + + /// The output of `cargo build --message-format=json` was not valid UTF-8 + #[error("Output of kernel build with --message-format=json is not valid UTF-8:\n{0}")] + BuildJsonOutputInvalidUtf8(std::string::FromUtf8Error), + /// The output of `cargo build --message-format=json` was not valid JSON + #[error("Output of kernel build with --message-format=json is not valid JSON:\n{0}")] + BuildJsonOutputInvalidJson(json::Error), +} + +/// Represents an error that occurred when creating a bootimage. +#[derive(Debug, Error)] +#[non_exhaustive] +pub enum CreateBootimageError { + /// Failed to build the bootloader. + #[error("An error occurred while trying to build the bootloader: {0}")] + Bootloader(#[from] BootloaderError), + + /// Error while running `cargo metadata` + #[error("Error while running `cargo metadata` for current project: {0:?}")] + CargoMetadata(#[from] cargo_metadata::Error), + + /// Building the bootloader failed + #[error("Bootloader build failed.\nStderr: {}", String::from_utf8_lossy(.stderr))] + BootloaderBuildFailed { + /// The `cargo build` output to standard error + stderr: Vec, + }, + + /// Disk image creation failed + #[error("An error occurred while trying to create the disk image: {0}")] + DiskImage(#[from] DiskImageError), + + /// An unexpected I/O error occurred + #[error("I/O error: {message}:\n{error}")] + Io { + /// Desciption of the failed I/O operation + message: &'static str, + /// The I/O error that occurred + error: io::Error, + }, + + /// The output of `cargo build --message-format=json` was not valid UTF-8 + #[error("Output of bootloader build with --message-format=json is not valid UTF-8:\n{0}")] + BuildJsonOutputInvalidUtf8(std::string::FromUtf8Error), + /// The output of `cargo build --message-format=json` was not valid JSON + #[error("Output of bootloader build with --message-format=json is not valid JSON:\n{0}")] + BuildJsonOutputInvalidJson(json::Error), +} + +/// There is something wrong with the bootloader dependency. +#[derive(Debug, Error)] +pub enum BootloaderError { + /// Bootloader dependency not found + #[error( + "Bootloader dependency not found\n\n\ + You need to add a dependency on a crate named `bootloader` in your Cargo.toml." + )] + BootloaderNotFound, + + /// Bootloader dependency has not the right format + #[error("The `bootloader` dependency has not the right format: {0}")] + BootloaderInvalid(String), + + /// Could not find kernel package in cargo metadata + #[error( + "Could not find package with manifest path `{manifest_path}` in cargo metadata output" + )] + KernelPackageNotFound { + /// The manifest path of the kernel package + manifest_path: PathBuf, + }, + + /// Could not find some required information in the `cargo metadata` output + #[error("Could not find required key `{key}` in cargo metadata output")] + CargoMetadataIncomplete { + /// The required key that was not found + key: String, + }, +} + +/// Creating the disk image failed. +#[derive(Debug, Error)] +pub enum DiskImageError { + /// The `llvm-tools-preview` rustup component was not found + #[error( + "Could not find the `llvm-tools-preview` rustup component.\n\n\ + You can install by executing `rustup component add llvm-tools-preview`." + )] + LlvmToolsNotFound, + + /// There was another problem locating the `llvm-tools-preview` rustup component + #[error("Failed to locate the `llvm-tools-preview` rustup component: {0:?}")] + LlvmTools(llvm_tools::Error), + + /// The llvm-tools component did not contain the required `llvm-objcopy` executable + #[error("Could not find `llvm-objcopy` in the `llvm-tools-preview` rustup component.")] + LlvmObjcopyNotFound, + + /// The `llvm-objcopy` command failed + #[error("Failed to run `llvm-objcopy`: {}", String::from_utf8_lossy(.stderr))] + ObjcopyFailed { + /// The output of `llvm-objcopy` to standard error + stderr: Vec, + }, + + /// An unexpected I/O error occurred + #[error("I/O error: {message}:\n{error}")] + Io { + /// Desciption of the failed I/O operation + message: &'static str, + /// The I/O error that occurred + error: io::Error, + }, +} + +impl From for DiskImageError { + fn from(err: llvm_tools::Error) -> Self { + match err { + llvm_tools::Error::NotFound => DiskImageError::LlvmToolsNotFound, + other => DiskImageError::LlvmTools(other), + } + } +} diff --git a/src/builder/mod.rs b/src/builder/mod.rs new file mode 100644 index 0000000..3acc5e6 --- /dev/null +++ b/src/builder/mod.rs @@ -0,0 +1,226 @@ +//! Provides functions to build the kernel and the bootloader. + +use crate::config::Config; +use cargo_metadata::Metadata; +use error::{BootloaderError, BuildKernelError, BuilderError, CreateBootimageError}; +use std::{ + path::{Path, PathBuf}, + process, +}; + +/// Provides the build command for the bootloader. +mod bootloader; +/// Provides a function to create the bootable disk image. +mod disk_image; +/// Contains the errors types returned by the `Builder` methods. +pub mod error; + +/// Allows building the kernel and creating a bootable disk image with it. +pub struct Builder { + manifest_path: PathBuf, + project_metadata: Option, +} + +impl Builder { + /// Creates a new builder for the project at the given manifest path + /// + /// If None is passed for `manifest_path`, it is automatically searched. + pub fn new(manifest_path: Option) -> Result { + let manifest_path = match manifest_path.or_else(|| { + std::env::var("CARGO_MANIFEST_DIR") + .ok() + .map(|dir| Path::new(&dir).join("Cargo.toml")) + }) { + Some(path) => path, + None => { + println!("WARNING: `CARGO_MANIFEST_DIR` env variable not set"); + locate_cargo_manifest::locate_manifest()? + } + }; + + Ok(Builder { + manifest_path, + project_metadata: None, + }) + } + + /// Returns the path to the Cargo.toml file of the project. + pub fn manifest_path(&self) -> &Path { + &self.manifest_path + } + + /// Builds the kernel by executing `cargo build` with the given arguments. + /// + /// Returns a list of paths to all built executables. For crates with only a single binary, + /// the returned list contains only a single element. + /// + /// If the quiet argument is set to true, all output to stdout is suppressed. + pub fn build_kernel( + &mut self, + args: &[String], + config: &Config, + quiet: bool, + ) -> Result, BuildKernelError> { + if !quiet { + println!("Building kernel"); + } + + // try to build kernel + let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned()); + let mut cmd = process::Command::new(&cargo); + cmd.args(&config.build_command); + cmd.args(args); + if !quiet { + cmd.stdout(process::Stdio::inherit()); + cmd.stderr(process::Stdio::inherit()); + } + let output = cmd.output().map_err(|err| BuildKernelError::Io { + message: "failed to execute kernel build", + error: err, + })?; + if !output.status.success() { + if config.build_command.starts_with(&["xbuild".into()]) { + // try executing `cargo xbuild --help` to check whether cargo-xbuild is installed + let mut help_command = process::Command::new("cargo"); + help_command.arg("xbuild").arg("--help"); + help_command.stdout(process::Stdio::null()); + help_command.stderr(process::Stdio::null()); + if let Ok(help_exit_status) = help_command.status() { + if !help_exit_status.success() { + return Err(BuildKernelError::XbuildNotFound); + } + } + } + return Err(BuildKernelError::BuildFailed { + stderr: output.stderr, + }); + } + + // Retrieve binary paths + let mut cmd = process::Command::new(cargo); + cmd.args(&config.build_command); + cmd.args(args); + cmd.arg("--message-format").arg("json"); + let output = cmd.output().map_err(|err| BuildKernelError::Io { + message: "failed to execute kernel build with json output", + error: err, + })?; + if !output.status.success() { + return Err(BuildKernelError::BuildFailed { + stderr: output.stderr, + }); + } + let mut executables = Vec::new(); + for line in String::from_utf8(output.stdout) + .map_err(BuildKernelError::BuildJsonOutputInvalidUtf8)? + .lines() + { + let mut artifact = + json::parse(line).map_err(BuildKernelError::BuildJsonOutputInvalidJson)?; + if let Some(executable) = artifact["executable"].take_string() { + executables.push(PathBuf::from(executable)); + } + } + + Ok(executables) + } + + /// Creates a bootimage by combining the given kernel binary with the bootloader. + /// + /// Places the resulting bootable disk image at the given `output_bin_path`. + /// + /// If the quiet argument is set to true, all output to stdout is suppressed. + pub fn create_bootimage( + &mut self, + kernel_manifest_path: &Path, + bin_path: &Path, + output_bin_path: &Path, + quiet: bool, + ) -> Result<(), CreateBootimageError> { + let bootloader_build_config = bootloader::BuildConfig::from_metadata( + self.project_metadata()?, + kernel_manifest_path, + bin_path, + )?; + + // build bootloader + if !quiet { + println!("Building bootloader"); + } + let mut cmd = bootloader_build_config.build_command(); + if !quiet { + cmd.stdout(process::Stdio::inherit()); + cmd.stderr(process::Stdio::inherit()); + } + let output = cmd.output().map_err(|err| CreateBootimageError::Io { + message: "failed to execute bootloader build command", + error: err, + })?; + if !output.status.success() { + return Err(CreateBootimageError::BootloaderBuildFailed { + stderr: output.stderr, + }); + } + + // Retrieve binary path + let mut cmd = bootloader_build_config.build_command(); + cmd.arg("--message-format").arg("json"); + let output = cmd.output().map_err(|err| CreateBootimageError::Io { + message: "failed to execute bootloader build command with json output", + error: err, + })?; + if !output.status.success() { + return Err(CreateBootimageError::BootloaderBuildFailed { + stderr: output.stderr, + }); + } + let mut bootloader_elf_path = None; + for line in String::from_utf8(output.stdout) + .map_err(CreateBootimageError::BuildJsonOutputInvalidUtf8)? + .lines() + { + let mut artifact = + json::parse(line).map_err(CreateBootimageError::BuildJsonOutputInvalidJson)?; + if let Some(executable) = artifact["executable"].take_string() { + if bootloader_elf_path + .replace(PathBuf::from(executable)) + .is_some() + { + return Err(BootloaderError::BootloaderInvalid( + "bootloader has multiple executables".into(), + ) + .into()); + } + } + } + let bootloader_elf_path = bootloader_elf_path.ok_or_else(|| { + BootloaderError::BootloaderInvalid("bootloader has no executable".into()) + })?; + + disk_image::create_disk_image(&bootloader_elf_path, output_bin_path)?; + + Ok(()) + } + + /// Returns the cargo metadata package that contains the given binary. + pub fn kernel_package_for_bin( + &mut self, + kernel_bin_name: &str, + ) -> Result, cargo_metadata::Error> { + Ok(self.project_metadata()?.packages.iter().find(|p| { + p.targets + .iter() + .any(|t| t.name == kernel_bin_name && t.kind.iter().any(|k| k == "bin")) + })) + } + + fn project_metadata(&mut self) -> Result<&Metadata, cargo_metadata::Error> { + if let Some(ref metadata) = self.project_metadata { + return Ok(metadata); + } + let metadata = cargo_metadata::MetadataCommand::new() + .manifest_path(&self.manifest_path) + .exec()?; + Ok(self.project_metadata.get_or_insert(metadata)) + } +} diff --git a/src/config.rs b/src/config.rs index 54ed547..3bed52a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,34 +1,60 @@ -use std::path::{Path, PathBuf}; -use Error; +//! Parses the `package.metadata.bootimage` configuration table + +use anyhow::{anyhow, Context, Result}; +use std::path::Path; use toml::Value; +/// Represents the `package.metadata.bootimage` configuration table +/// +/// The bootimage crate can be configured through a `package.metadata.bootimage` table +/// in the `Cargo.toml` file of the kernel. This struct represents the parsed configuration +/// options. #[derive(Debug, Clone)] +#[non_exhaustive] pub struct Config { - pub manifest_path: PathBuf, - pub default_target: Option, - pub output: Option, - pub bootloader: BootloaderConfig, - pub minimum_image_size: Option, + /// The cargo subcommand that is used for building the kernel for `cargo bootimage`. + /// + /// Defaults to `build`. + pub build_command: Vec, + /// The run command that is invoked on `bootimage run` or `bootimage runner` + /// + /// The substring "{}" will be replaced with the path to the bootable disk image. pub run_command: Vec, + /// Additional arguments passed to the runner for not-test binaries + /// + /// Applies to `bootimage run` and `bootimage runner`. + pub run_args: Option>, + /// Additional arguments passed to the runner for test binaries + /// + /// Applies to `bootimage runner`. + pub test_args: Option>, + /// The timeout for running an test through `bootimage test` or `bootimage runner` in seconds + pub test_timeout: u32, + /// An exit code that should be considered as success for test executables (applies to + /// `bootimage runner`) + pub test_success_exit_code: Option, + /// Whether the `-no-reboot` flag should be passed to test executables + /// + /// Defaults to `true` + pub test_no_reboot: bool, } -#[derive(Debug, Clone)] -pub struct BootloaderConfig { - pub name: String, - pub precompiled: bool, - pub target: PathBuf, - pub version: Option, - pub git: Option, - pub branch: Option, - pub path: Option, +/// Reads the configuration from a `package.metadata.bootimage` in the given Cargo.toml. +pub fn read_config(manifest_path: &Path) -> Result { + read_config_inner(manifest_path).context("Failed to read bootimage configuration") } -pub(crate) fn read_config(manifest_path: PathBuf) -> Result { +fn read_config_inner(manifest_path: &Path) -> Result { use std::{fs::File, io::Read}; let cargo_toml: Value = { let mut content = String::new(); - File::open(&manifest_path)?.read_to_string(&mut content)?; - content.parse()? + File::open(manifest_path) + .context("Failed to open Cargo.toml")? + .read_to_string(&mut content) + .context("Failed to read Cargo.toml")?; + content + .parse::() + .context("Failed to parse Cargo.toml")? }; let metadata = cargo_toml @@ -37,144 +63,92 @@ pub(crate) fn read_config(manifest_path: PathBuf) -> Result { .and_then(|table| table.get("bootimage")); let metadata = match metadata { None => { - return Ok(ConfigBuilder { - manifest_path: Some(manifest_path), - ..Default::default() - }.into()) + return Ok(ConfigBuilder::default().into()); } - Some(metadata) => metadata.as_table().ok_or(Error::Config(format!( - "Bootimage configuration invalid: {:?}", - metadata - )))?, + Some(metadata) => metadata + .as_table() + .ok_or_else(|| anyhow!("Bootimage configuration invalid: {:?}", metadata))?, }; - let mut config = ConfigBuilder { - manifest_path: Some(manifest_path), - ..Default::default() - }; + let mut config = ConfigBuilder::default(); for (key, value) in metadata { match (key.as_str(), value.clone()) { - ("default-target", Value::String(s)) => config.default_target = From::from(s), - ("output", Value::String(s)) => config.output = Some(PathBuf::from(s)), - ("bootloader", Value::Table(t)) => { - let mut bootloader_config = BootloaderConfigBuilder::default(); - for (key, value) in t { - match (key.as_str(), value) { - ("name", Value::String(s)) => bootloader_config.name = From::from(s), - ("precompiled", Value::Boolean(b)) => { - bootloader_config.precompiled = From::from(b) - } - ("target", Value::String(s)) => { - bootloader_config.target = Some(PathBuf::from(s)) - } - ("version", Value::String(s)) => bootloader_config.version = From::from(s), - ("git", Value::String(s)) => bootloader_config.git = From::from(s), - ("branch", Value::String(s)) => bootloader_config.branch = From::from(s), - ("path", Value::String(s)) => { - bootloader_config.path = Some(Path::new(&s).canonicalize()?); - } - (key, value) => Err(Error::Config(format!( - "unexpected \ - `package.metadata.bootimage.bootloader` key `{}` with value `{}`", - key, value - )))?, - } - } - config.bootloader = Some(bootloader_config); + ("test-timeout", Value::Integer(timeout)) if timeout.is_negative() => { + return Err(anyhow!("test-timeout must not be negative")) + } + ("test-timeout", Value::Integer(timeout)) => { + config.test_timeout = Some(timeout as u32); + } + ("test-success-exit-code", Value::Integer(exit_code)) => { + config.test_success_exit_code = Some(exit_code as i32); } - ("minimum-image-size", Value::Integer(x)) => { - if x >= 0 { - config.minimum_image_size = Some((x * 1024 * 1024) as u64); // MiB -> Byte - } else { - Err(Error::Config(format!( - "unexpected `package.metadata.bootimage` \ - key `minimum-image-size` with negative value `{}`", - value - )))? - } + ("build-command", Value::Array(array)) => { + config.build_command = Some(parse_string_array(array, "build-command")?); } ("run-command", Value::Array(array)) => { - let mut command = Vec::new(); - for value in array { - match value { - Value::String(s) => command.push(s), - _ => Err(Error::Config(format!( - "run-command must be a list of strings" - )))?, - } - } - config.run_command = Some(command); + config.run_command = Some(parse_string_array(array, "run-command")?); } - (key, value) => Err(Error::Config(format!( - "unexpected `package.metadata.bootimage` \ + ("run-args", Value::Array(array)) => { + config.run_args = Some(parse_string_array(array, "run-args")?); + } + ("test-args", Value::Array(array)) => { + config.test_args = Some(parse_string_array(array, "test-args")?); + } + ("test-no-reboot", Value::Boolean(no_reboot)) => { + config.test_no_reboot = Some(no_reboot); + } + (key, value) => { + return Err(anyhow!( + "unexpected `package.metadata.bootimage` \ key `{}` with value `{}`", - key, value - )))?, + key, + value + )) + } } } Ok(config.into()) } -#[derive(Default)] -struct ConfigBuilder { - manifest_path: Option, - default_target: Option, - output: Option, - bootloader: Option, - minimum_image_size: Option, - run_command: Option>, +fn parse_string_array(array: Vec, prop_name: &str) -> Result> { + let mut parsed = Vec::new(); + for value in array { + match value { + Value::String(s) => parsed.push(s), + _ => return Err(anyhow!("{} must be a list of strings", prop_name)), + } + } + Ok(parsed) } #[derive(Default)] -struct BootloaderConfigBuilder { - name: Option, - precompiled: Option, - target: Option, - version: Option, - branch: Option, - git: Option, - path: Option, +struct ConfigBuilder { + build_command: Option>, + run_command: Option>, + run_args: Option>, + test_args: Option>, + test_timeout: Option, + test_success_exit_code: Option, + test_no_reboot: Option, } impl Into for ConfigBuilder { fn into(self) -> Config { - let default_bootloader_config = BootloaderConfigBuilder { - precompiled: Some(true), - ..Default::default() - }; Config { - manifest_path: self.manifest_path.expect("manifest path must be set"), - default_target: self.default_target, - output: self.output, - bootloader: self.bootloader.unwrap_or(default_bootloader_config).into(), - minimum_image_size: self.minimum_image_size, - run_command: self.run_command.unwrap_or(vec![ - "qemu-system-x86_64".into(), - "-drive".into(), - "format=raw,file={}".into(), - ]), - } - } -} - -impl Into for BootloaderConfigBuilder { - fn into(self) -> BootloaderConfig { - let precompiled = self.precompiled.unwrap_or(false); - let default_name = if precompiled { - "bootloader_precompiled" - } else { - "bootloader" - }; - BootloaderConfig { - name: self.name.unwrap_or(default_name.into()), - precompiled, - target: self.target - .unwrap_or(PathBuf::from("x86_64-bootloader.json")), - version: self.version, - git: self.git, - branch: self.branch, - path: self.path, + build_command: self.build_command.unwrap_or_else(|| vec!["build".into()]), + run_command: self.run_command.unwrap_or_else(|| { + vec![ + "qemu-system-x86_64".into(), + "-drive".into(), + "format=raw,file={}".into(), + ] + }), + run_args: self.run_args, + test_args: self.test_args, + test_timeout: self.test_timeout.unwrap_or(60 * 5), + test_success_exit_code: self.test_success_exit_code, + test_no_reboot: self.test_no_reboot.unwrap_or(true), } } } diff --git a/src/help/build_help.txt b/src/help/build_help.txt deleted file mode 100644 index 3009294..0000000 --- a/src/help/build_help.txt +++ /dev/null @@ -1,33 +0,0 @@ -Creates a bootable disk image from a Rust kernel - -USAGE: - bootimage build [BUILD_OPTS] Create a bootable disk image - - (for other forms of usage see `bootimage --help`) - -BUILD_OPTS: - --update-bootloader Update the bootloader dependency. - - Any additional options are directly passed to `cargo build` (see - `cargo build --help` for possible options). After building, a bootloader - is downloaded and built, and then combined with the kernel into a bootable - disk image. - -CONFIGURATION: - The bootloader and the behavior of `bootimage build` can be configured - through a `[package.metadata.bootimage]` table in the `Cargo.toml`. The - following options are available to configure the build: - - [package.metadata.bootimage] - default-target = "" This target is used if no `--target` is passed - output = "bootimage.bin" The output file name - minimum-image-size = 0 The minimum output file size (in MiB) - - [package.metadata.bootimage.bootloader] - name = "bootloader" The bootloader crate name - version = "" The bootloader version that should be used - git = "" Use the bootloader from this git repository - branch = "" The git branch to use (defaults to master) - path = "" Use the bootloader from this local path - precompiled = false Whether the bootloader crate is precompiled - target = "x86_64-bootloader.json" Target triple for compiling the bootloader diff --git a/src/help/cargo_bootimage_help.txt b/src/help/cargo_bootimage_help.txt new file mode 100644 index 0000000..16647ef --- /dev/null +++ b/src/help/cargo_bootimage_help.txt @@ -0,0 +1,23 @@ +Creates a bootable disk image from a Rust kernel + +USAGE: + cargo bootimage [BUILD_OPTS] Create a bootable disk image + + (for other forms of usage see `bootimage --help`) + +BUILD_OPTS: + Any options are directly passed to `cargo build` (see + `cargo build --help` for possible options). After building, a bootloader + is downloaded and built, and then combined with the kernel into a bootable + disk image. + +CONFIGURATION: + The behavior of `cargo bootimage` can be configured through a + `[package.metadata.bootimage]` table in the `Cargo.toml`. The + following options are available to configure the build behavior: + + [package.metadata.bootimage] + # The cargo subcommand that will be used for building the kernel. + # + # For building using the `cargo-xbuild` crate, set this to `xbuild`. + build-command = ["build"] diff --git a/src/help/help.txt b/src/help/help.txt index 9cc5845..1325a18 100644 --- a/src/help/help.txt +++ b/src/help/help.txt @@ -1,26 +1,11 @@ Creates a bootable disk image from a Rust kernel USAGE: - bootimage [OPTIONS] Help and version information - bootimage build [BUILD_OPTS] Create a bootable disk image - bootimage run [BUILD_OPTS] -- [RUN_OPTS] Build and run a disk image + cargo bootimage [BUILD_OPTS] Create a bootable disk image + bootimage runner EXECUTABLE [RUN_OPTS] Convert and run an executable -OPTIONS: - -h, --help Prints help information and exit - ---version Prints version information and exit +For more information about a subcommand run `[subcommand] --help`. -BUILD_OPTS: - --update-bootloader Update the bootloader dependency. - - Any additional options are directly passed to `cargo build` (see - `cargo build --help` for possible options). After building, a bootloader - is downloaded and built, and then combined with the kernel into a bootable - disk image. - - For configuration options see `bootimage build --help`. - -RUN_OPTS: - Any options are directly passed to the run command. Note that the run - options must be separated from the build options by a "--". - - For configuration options see `bootimage run --help`. +GENERAL OPTIONS: + -h, --help Prints help information and exit + --version Prints version information and exit diff --git a/src/help/mod.rs b/src/help/mod.rs index 279b9a9..d29dd9b 100644 --- a/src/help/mod.rs +++ b/src/help/mod.rs @@ -1,24 +1,22 @@ -use std::process; - const HELP: &str = include_str!("help.txt"); -const BUILD_HELP: &str = include_str!("build_help.txt"); -const RUN_HELP: &str = include_str!("run_help.txt"); +const CARGO_BOOTIMAGE_HELP: &str = include_str!("cargo_bootimage_help.txt"); +const RUNNER_HELP: &str = include_str!("runner_help.txt"); -pub(crate) fn help() { +/// Prints a general help text. +pub fn print_help() { print!("{}", HELP); } -pub(crate) fn build_help() { - print!("{}", BUILD_HELP); +/// Prints the help for the `cargo bootimage` command. +pub fn print_cargo_bootimage_help() { + print!("{}", CARGO_BOOTIMAGE_HELP); } - -pub(crate) fn run_help() { - print!("{}", RUN_HELP); +/// Prints the help for the `bootimage runner` command. +pub fn print_runner_help() { + print!("{}", RUNNER_HELP); } -pub(crate) fn no_subcommand() -> ! { - println!("Please invoke `bootimage` with a subcommand (e.g. `bootimage build`)."); - println!(); - println!("See `bootimage --help` for more information."); - process::exit(1); +/// Prints the version of this crate. +pub fn print_version() { + println!("bootimage {}", env!("CARGO_PKG_VERSION")); } diff --git a/src/help/run_help.txt b/src/help/run_help.txt deleted file mode 100644 index 2105125..0000000 --- a/src/help/run_help.txt +++ /dev/null @@ -1,21 +0,0 @@ -Creates a bootable disk image from a Rust kernel - -USAGE: - bootimage run [BUILD_OPTS] -- [RUN_OPTS] Build and run a disk image - - (for other forms of usage see `bootimage --help`) - (for BUILD_OPTS see `bootimage build --help`) - -RUN_OPTS: - Any options are directly passed to the run command. Note that the run - options must be separated from the build options by a "--". - -CONFIGURATION: - The behavior of `bootimage run` can be configured through a - `[package.metadata.bootimage]` table in the `Cargo.toml`. The - following options are available to configure run behavior: - - [package.metadata.bootimage] - # The command invoked on `bootimage run` - # (the "{}" will be replaced with the path to the bootable disk image) - run-command = ["qemu-system-x86_64", "-drive", "format=raw,file={}"] diff --git a/src/help/runner_help.txt b/src/help/runner_help.txt new file mode 100644 index 0000000..a526b7e --- /dev/null +++ b/src/help/runner_help.txt @@ -0,0 +1,32 @@ +Creates a bootable disk image from a Rust kernel and launches it in QEMU + +USAGE: + bootimage runner EXECUTABLE [ARGS] Convert and run the given EXECUTABLE + + (for other forms of usage see `bootimage --help`) + + This subcommand can be used as a target runner in a `.cargo/config` file: + ``` + [target.'cfg(target_os = "none")'] + runner = "bootimage runner" + ``` + + All ARGS are passed to the run command. + +CONFIGURATION: + The behavior of `bootimage runner` can be configured through a + `[package.metadata.bootimage]` table in the `Cargo.toml`. The + following options are available to configure run behavior: + + [package.metadata.bootimage] + # The command invoked with the created bootimage (the "{}" will be replaced + # with the path to the bootable disk image) + run-command = ["qemu-system-x86_64", "-drive", "format=raw,file={}"] + # Additional arguments passed to the run command for non-test executables + run-args = [] + # Additional arguments passed to the run command for test executables + test-args = [] + # An exit code that should be considered as success for test executables + test-success-exit-code = {integer} + # The timeout for running a test (in seconds) + test-timeout = 300 diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..d682726 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,13 @@ +//! Provides functions to create a bootable OS image from a kernel binary. +//! +//! This crate is mainly built as a binary tool. Run `cargo install bootimage` to install it. + +#![warn(missing_docs)] + +pub mod args; +pub mod builder; +pub mod config; +pub mod run; + +/// Contains help messages for the command line application. +pub mod help; diff --git a/src/main.rs b/src/main.rs index cfcf9c3..3cb34ed 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,77 +1,111 @@ -extern crate byteorder; -extern crate cargo_metadata; -extern crate tempdir; -extern crate toml; -extern crate xmas_elf; -extern crate wait_timeout; +/// Executable for `bootimage runner`. +use anyhow::{anyhow, Context, Result}; +use bootimage::{ + args::{RunnerArgs, RunnerCommand}, + builder::Builder, + config, help, run, +}; +use std::process; +use std::{env, path::Path}; -use std::{io, process}; -use args::Args; +pub fn main() -> Result<()> { + let mut raw_args = env::args(); -mod args; -mod config; -mod build; -mod test; -mod help; + let executable_name = raw_args + .next() + .ok_or_else(|| anyhow!("no first argument (executable name)"))?; + let file_stem = Path::new(&executable_name) + .file_stem() + .and_then(|s| s.to_str()); + if file_stem != Some("bootimage") { + return Err(anyhow!( + "Unexpected executable name: expected `bootimage`, got: `{:?}`", + file_stem + )); + } + match raw_args.next().as_deref() { + Some("runner") => {}, + Some("--help") | Some("-h") => { + help::print_help(); + return Ok(()) + } + Some("--version") => { + help::print_version(); + return Ok(()) + } + Some(other) => return Err(anyhow!( + "Unsupported subcommand `{:?}`. See `bootimage --help` for an overview of supported subcommands.", other + )), + None => return Err(anyhow!( + "Please invoke bootimage with a subcommand. See `bootimage --help` for more information." + )), + } -enum Command { - NoSubcommand, - Build(Args), - Run(Args), - Test(Args), - Help, - BuildHelp, - RunHelp, - TestHelp, - Version, -} + let exit_code = match RunnerCommand::parse_args(raw_args)? { + RunnerCommand::Runner(args) => Some(runner(args)?), + RunnerCommand::Version => { + help::print_version(); + None + } + RunnerCommand::Help => { + help::print_runner_help(); + None + } + }; -pub fn main() { - use std::io::Write; - if let Err(err) = run() { - writeln!(io::stderr(), "Error: {:?}", err).unwrap(); - process::exit(1); + if let Some(code) = exit_code { + process::exit(code); } -} -#[derive(Debug)] -pub enum Error { - Config(String), - Bootloader(String, io::Error), - Io(io::Error), - Toml(toml::de::Error), - CargoMetadata(cargo_metadata::Error), + Ok(()) } -impl From for Error { - fn from(other: io::Error) -> Self { - Error::Io(other) - } -} +pub(crate) fn runner(args: RunnerArgs) -> Result { + let mut builder = Builder::new(None)?; + let config = config::read_config(builder.manifest_path())?; + let exe_parent = args + .executable + .parent() + .ok_or_else(|| anyhow!("kernel executable has no parent"))?; + let is_doctest = exe_parent + .file_name() + .ok_or_else(|| anyhow!("kernel executable's parent has no file name"))? + .to_str() + .ok_or_else(|| anyhow!("kernel executable's parent file name is not valid UTF-8"))? + .starts_with("rustdoctest"); + let is_test = is_doctest || exe_parent.ends_with("deps"); -impl From for Error { - fn from(other: toml::de::Error) -> Self { - Error::Toml(other) - } -} + let bin_name = args + .executable + .file_stem() + .ok_or_else(|| anyhow!("kernel executable has no file stem"))? + .to_str() + .ok_or_else(|| anyhow!("kernel executable file stem is not valid UTF-8"))?; -impl From for Error { - fn from(other: cargo_metadata::Error) -> Self { - Error::CargoMetadata(other) - } -} + let output_bin_path = exe_parent.join(format!("bootimage-{}.bin", bin_name)); + let executable_canonicalized = args.executable.canonicalize().with_context(|| { + format!( + "failed to canonicalize executable path `{}`", + args.executable.display(), + ) + })?; -fn run() -> Result<(), Error> { - let command = args::parse_args(); - match command { - Command::NoSubcommand => help::no_subcommand(), - Command::Build(args) => build::build(args), - Command::Run(args) => build::run(args), - Command::Test(args) => test::test(args), - Command::Help => Ok(help::help()), - Command::BuildHelp => Ok(help::build_help()), - Command::RunHelp => Ok(help::run_help()), - Command::TestHelp => unimplemented!(), - Command::Version => Ok(println!("bootimage {}", env!("CARGO_PKG_VERSION"))), - } + // Cargo sets a CARGO_MANIFEST_DIR environment variable for all runner + // executables. This variable contains the path to the Cargo.toml of the + // crate that the executable belongs to (i.e. not the project root + // manifest for workspace projects) + let manifest_dir = env::var("CARGO_MANIFEST_DIR") + .context("Failed to read CARGO_MANIFEST_DIR environment variable")?; + let kernel_manifest_path = Path::new(&manifest_dir).join("Cargo.toml"); + + builder.create_bootimage( + &kernel_manifest_path, + &executable_canonicalized, + &output_bin_path, + args.quiet, + )?; + + let exit_code = run::run(config, args, &output_bin_path, is_test)?; + + Ok(exit_code) } diff --git a/src/run.rs b/src/run.rs new file mode 100644 index 0000000..e77c0f9 --- /dev/null +++ b/src/run.rs @@ -0,0 +1,147 @@ +//! Provides a function for running a disk image in QEMU. + +use crate::{args::RunnerArgs, config::Config}; +use std::{io, path::Path, process, time::Duration}; +use thiserror::Error; +use wait_timeout::ChildExt; + +/// Run the given disk image in QEMU. +/// +/// Automatically takes into account the runner arguments and the run/test +/// commands defined in the given `Config`. Since test executables are treated +/// differently (run with a timeout and match exit status), the caller needs to +/// specify whether the given disk image is a test or not. +pub fn run( + config: Config, + args: RunnerArgs, + image_path: &Path, + is_test: bool, +) -> Result { + let mut run_command: Vec<_> = config + .run_command + .iter() + .map(|arg| arg.replace("{}", &format!("{}", image_path.display()))) + .collect(); + if is_test { + if config.test_no_reboot { + run_command.push("-no-reboot".to_owned()); + } + if let Some(args) = config.test_args { + run_command.extend(args); + } + } else if let Some(args) = config.run_args { + run_command.extend(args); + } + if let Some(args) = args.runner_args { + run_command.extend(args); + } + + if !args.quiet { + println!("Running: `{}`", run_command.join(" ")); + } + let mut command = process::Command::new(&run_command[0]); + command.args(&run_command[1..]); + + let exit_code = if is_test { + let mut child = command.spawn().map_err(|error| RunError::Io { + context: IoErrorContext::QemuTestCommand { + command: format!("{:?}", command), + }, + error, + })?; + let timeout = Duration::from_secs(config.test_timeout.into()); + match child + .wait_timeout(timeout) + .map_err(context(IoErrorContext::WaitWithTimeout))? + { + None => { + child.kill().map_err(context(IoErrorContext::KillQemu))?; + child.wait().map_err(context(IoErrorContext::WaitForQemu))?; + return Err(RunError::TestTimedOut); + } + Some(exit_status) => { + #[cfg(unix)] + { + if exit_status.code().is_none() { + use std::os::unix::process::ExitStatusExt; + if let Some(signal) = exit_status.signal() { + eprintln!("QEMU process was terminated by signal {}", signal); + } + } + } + let qemu_exit_code = exit_status.code().ok_or(RunError::NoQemuExitCode)?; + match config.test_success_exit_code { + Some(code) if qemu_exit_code == code => 0, + Some(_) if qemu_exit_code == 0 => 1, + _ => qemu_exit_code, + } + } + } + } else { + let status = command.status().map_err(|error| RunError::Io { + context: IoErrorContext::QemuRunCommand { + command: format!("{:?}", command), + }, + error, + })?; + status.code().unwrap_or(1) + }; + + Ok(exit_code) +} + +/// Running the disk image failed. +#[derive(Debug, Error)] +pub enum RunError { + /// Test timed out + #[error("Test timed out")] + TestTimedOut, + + /// Failed to read QEMU exit code + #[error("Failed to read QEMU exit code")] + NoQemuExitCode, + + /// An I/O error occurred + #[error("{context}: An I/O error occurred: {error}")] + Io { + /// The operation that caused the I/O error. + context: IoErrorContext, + /// The I/O error that occurred. + error: io::Error, + }, +} + +/// An I/O error occurred while trying to run the disk image. +#[derive(Debug, Error)] +pub enum IoErrorContext { + /// QEMU command for non-test failed + #[error("Failed to execute QEMU run command `{command}`")] + QemuRunCommand { + /// The QEMU command that was executed + command: String, + }, + + /// QEMU command for test failed + #[error("Failed to execute QEMU test command `{command}`")] + QemuTestCommand { + /// The QEMU command that was executed + command: String, + }, + + /// Waiting for test with timeout failed + #[error("Failed to wait with timeout")] + WaitWithTimeout, + + /// Failed to kill QEMU + #[error("Failed to kill QEMU")] + KillQemu, + + /// Failed to wait for QEMU process + #[error("Failed to wait for QEMU process")] + WaitForQemu, +} + +/// Helper function for IO error construction +fn context(context: IoErrorContext) -> impl FnOnce(io::Error) -> RunError { + |error| RunError::Io { context, error } +} diff --git a/src/test.rs b/src/test.rs deleted file mode 100644 index eb7455a..0000000 --- a/src/test.rs +++ /dev/null @@ -1,106 +0,0 @@ -use std::{fs, io, process}; -use Error; -use args::Args; -use build; -use wait_timeout::ChildExt; -use std::time::Duration; -use std::io::Write; - -pub(crate) fn test(args: Args) -> Result<(), Error> { - let (args, config, metadata, root_dir, out_dir) = build::common_setup(args)?; - - let test_args = args.clone(); - let test_run_command = vec![ - "qemu-system-x86_64".into(), - "-drive".into(), - "format=raw,file={}".into(), - "-device".into(), - "isa-debug-exit,iobase=0xf4,iosize=0x04".into(), - "-display".into(), - "none".into(), - "-serial".into(), - "file:{}-output.txt".into(), - ]; - let test_config = { - let mut test_config = config.clone(); - test_config.output = None; - test_config.run_command = test_run_command; - test_config - }; - - let mut tests = Vec::new(); - - assert_eq!(metadata.packages.len(), 1, "Only crates with one package are supported"); - let target_iter = metadata.packages[0].targets.iter(); - for target in target_iter.filter(|t| t.kind == ["bin"] && t.name.starts_with("test-")) { - println!("{}", target.name); - - let mut target_args = test_args.clone(); - target_args.set_bin_name(target.name.clone()); - let test_path = build::build_impl(&target_args, &test_config, &metadata, &root_dir, &out_dir, false)?; - - let test_result; - let output_file = format!("{}-output.txt", test_path.display()); - - let mut command = process::Command::new("qemu-system-x86_64"); - command.arg("-drive"); - command.arg(format!("format=raw,file={}", test_path.display())); - command.arg("-device"); - command.arg("isa-debug-exit,iobase=0xf4,iosize=0x04"); - command.arg("-display"); - command.arg("none"); - command.arg("-serial"); - command.arg(format!("file:{}", output_file)); - command.stderr(process::Stdio::null()); - let mut child = command.spawn()?; - let timeout = Duration::from_secs(60); - match child.wait_timeout(timeout)? { - None => { - child.kill()?; - child.wait()?; - test_result = TestResult::TimedOut; - writeln!(io::stderr(), "Timed Out")?; - } - Some(_) => { - let output = fs::read_to_string(output_file)?; - if output.starts_with("ok\n") { - test_result = TestResult::Ok; - println!("Ok"); - } else if output.starts_with("failed\n") { - test_result = TestResult::Failed; - writeln!(io::stderr(), "Failed:")?; - for line in output[7..].lines() { - writeln!(io::stderr(), " {}", line)?; - } - } else { - test_result = TestResult::Invalid; - writeln!(io::stderr(), "Failed: Invalid Output:")?; - for line in output.lines() { - writeln!(io::stderr(), " {}", line)?; - } - } - }, - } - println!(""); - - tests.push((target.name.clone(), test_result)) - } - - if tests.iter().all(|t| t.1 == TestResult::Ok) { - Ok(()) - } else { - writeln!(io::stderr(), "The following tests failed:")?; - for test in tests.iter().filter(|t| t.1 != TestResult::Ok) { - writeln!(io::stderr(), " {}: {:?}", test.0, test.1)?; - } - process::exit(1); - } -} - -#[derive(Debug, PartialEq, Eq)] -enum TestResult { - Ok, - Failed, - TimedOut, - Invalid, -}