diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..49b710c --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +./target +/build \ No newline at end of file diff --git a/.github/workflows/always.yml b/.github/workflows/always.yml new file mode 100644 index 0000000..c04a86d --- /dev/null +++ b/.github/workflows/always.yml @@ -0,0 +1,13 @@ +name: ci +on: [push] +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - name: dependencies + uses: actions/checkout@v3 + + - name: run + run: make ci + diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..7ed7918 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,39 @@ +name: prerelease +on: + push: + branches: + - 'main' +jobs: + release: + name: 'Build' + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: 'Install dependencies' + run: | + sudo apt -y update + sudo apt -y install musl-tools clang + rustup target add x86_64-unknown-linux-musl + + - name: 'Build artifacts' + run: make release + + - name: 'Upload thistle-yocto-build' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: dist/thistle-yocto-build-x86_64-unknown-linux-musl + asset_name: thistle-yocto-build + tag: edge + overwrite: true + + - name: 'Upload sha256sums.txt' + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: dist/sha256sums.txt + asset_name: sha256sums.txt + tag: edge + overwrite: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..84a2997 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,45 @@ +name: Release +on: + push: + tags: ["v[0-9]+.[0-9]+.[0-9]+*"] + +jobs: + release: + name: Build + runs-on: ubuntu-latest + steps: + - name: checkout + uses: actions/checkout@v3 + + - name: dependencies + run: | + sudo apt -y update + sudo apt -y install musl-tools clang + rustup target add x86_64-unknown-linux-musl + + - name: run + run: make release + + - name: upload thistle-yocto-build + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: dist/thistle-yocto-build-x86_64-unknown-linux-musl + asset_name: thistle-yocto-build + tag: ${{ github.ref }} + overwrite: true + + - name: upload sha256sums.txt + uses: svenstaro/upload-release-action@v2 + with: + repo_token: ${{ secrets.GITHUB_TOKEN }} + file: dist/sha256sums.txt + asset_name: sha256sums.txt + tag: ${{ github.ref }} + overwrite: true + + - name: release-tag + run: | + set -euxo pipefail + echo "RELEASE_VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_ENV + diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23d5d74 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target +/target_ra +/layers +/build +/test +/dist +.thistlebuild.sh \ No newline at end of file diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..aeb210b --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,4 @@ +fn_call_width=90 +attr_fn_like_width=90 +chain_width=90 +max_width=110 \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..9f63788 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "thistle-yocto-build", + "cargo": { + "args": ["build"], + "filter": { + "name": "thistle-yocto-build", + "kind": "bin" + } + }, + "args": ["build", "samples/qemuarm64.yml"], + "env": { + "DRYRUN": "1", + } + }, + ] +} \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS new file mode 100644 index 0000000..a46cf3d --- /dev/null +++ b/CODEOWNERS @@ -0,0 +1,3 @@ + +# See https://help.github.com/articles/about-codeowners/ +* @thistletech/engineering \ No newline at end of file diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9a2e93f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,132 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of + any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email address, + without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][Mozilla CoC]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][FAQ]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[Mozilla CoC]: https://github.com/mozilla/diversity +[FAQ]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..f730e15 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,18 @@ +# Contributing to Project + +## Code of conduct + +Thank you for considering contributing to the project. Please follow the [code +of conduct](./CODE_OF_CONDUCT.md) when conbributing. + +## How to contribute + +Pull requests are welcome. Please make an effort to follow existing code style +to maximize readability. Please make sure all test pass. + +Bug reports and feature requests through "Issues" are welcome. Please include +reproducing steps in a bug report. Please provide a detailed description in a +feature request. + +We require that all commits in this repository [be +signed](https://docs.github.com/en/authentication/managing-commit-signature-verification/signing-commits). diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9e2a699 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,941 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bindgen" +version = "0.64.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4243e6031260db77ede97ad86c27e501d646a27ab57b59a574f725d98ab1fb4" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "log", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", + "which", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "built" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b9c056b9ed43aee5e064b683aa1ec783e19c6acec7559e3ae931b7490472fbe" +dependencies = [ + "cargo-lock", +] + +[[package]] +name = "bumpalo" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" + +[[package]] +name = "cargo-lock" +version = "8.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "031718ddb8f78aa5def78a09e90defe30151d1f6c672f937af4dd916429ed996" +dependencies = [ + "semver", + "serde", + "toml", + "url", +] + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-integer", + "num-traits", + "time", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "clang-sys" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "3.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim", + "termcolor", + "textwrap", +] + +[[package]] +name = "clap_derive" +version = "3.2.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" +dependencies = [ + "heck", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2850f2f5a82cbf437dd5af4d49848fbdfc27c157c3d010345776f952765261c5" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "crypt3-sys" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed6520a0f1358f8be5e28cec07106ad6086427d52a8a3c64d072429e9fe39a71" +dependencies = [ + "bindgen", + "cc", +] + +[[package]] +name = "cxx" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90d59d9acd2a682b4e40605a242f6670eaa58c5957471cbf85e8aa6a0b97a5e8" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebfa40bda659dd5c864e65f4c9a2b0aff19bea56b017b9b77c73d3766a453a38" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "457ce6757c5c70dc6ecdbda6925b958aae7f959bda7d8fb9bde889e34a09dc03" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.90" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebf883b7aacd7b2aeb2a7b338648ee19f57c140d4ee8e52c68979c6b2f7f2263" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.53" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "winapi", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc" + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" + +[[package]] +name = "os_str_bytes" +version = "6.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regex" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48aaa5748ba571fb95cd2c85c09f629215d3a6ece942baa100950af03a34f733" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rpassword" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c9f5d2a0c3e2ea729ab3706d22217177770654c3ef5056b68b69d07332d3f5" +dependencies = [ + "libc", + "winapi", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "ryu" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" + +[[package]] +name = "scratch" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddccb15bcce173023b3fedd9436f882a0739b8dfb45e4f6b6002bee5929f61b2" + +[[package]] +name = "semver" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +dependencies = [ + "serde", +] + +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thistle-yocto-build" +version = "2.0.0" +dependencies = [ + "anyhow", + "built", + "chrono", + "clap", + "crypt3-sys", + "lazy_static", + "num_cpus", + "rand", + "rpassword", + "serde", + "serde_json", + "serde_yaml", + "thiserror", + "url", +] + +[[package]] +name = "time" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "toml" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f7f0dd8d50a853a531c426359045b1998f04219d88799810762cd4ad314234" +dependencies = [ + "serde", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "which" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +dependencies = [ + "either", + "libc", + "once_cell", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9c3ef6e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "thistle-yocto-build" +edition = "2021" +version = "2.0.0" + +[dependencies] +anyhow = "1.0.58" +clap = { version = "3.2.10", features = ["derive", "env"] } +serde_yaml = "0.8.25" +rpassword = "7.0.0" +rand = "0.8.5" +crypt3-sys = "0.1.0" +serde_json = "1.0.82" +url = "2.3.1" +chrono = "0.4.23" +serde = {version = "1.0.147", features = ["derive"]} +thiserror = "1.0.37" +num_cpus = "1.15.0" +lazy_static = "1.4.0" + +[build-dependencies.built] +version = "0.5" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..457c486 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +from archlinux:base-devel + +# pull deps +run pacman -Sy +run pacman --noconfirm -S base-devel git diffstat unzip texinfo python chrpath wget xterm rpcsvc-proto socat cpio inetutils rustup clang +run pacman --noconfirm -S musl + +run rustup install stable +run rustup target add x86_64-unknown-linux-musl + +# cache optimisation +run cargo search minisign + +# set local +run echo "en_US.UTF-8 UTF-8" > /etc/locale.gen +run locale-gen + +# install +run wget -q https://downloads.thistle.tech/thistle-build/edge/thistle-build + +# make builder user +run useradd -ms /bin/bash builder +user builder +workdir /home/builder + +# dummy creds for debug build +env THISTLE_YOCTO_BUILD_USERNAME a +env THISTLE_YOCTO_BUILD_PASSWORD a + +# set entrypoint with help +cmd ["thistle-yocto-build", "--help"] + +## Build +# $ docker build -t tyb . + +## Run +# $ docker run tyb thistle-yocto-build --help + +## Build Image +# $ \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4bc705b --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +STRIP := "-C link-arg=-s" + +check:: + cargo clippy --workspace -- -D warnings + cargo fmt --all + +check-enforce:: + cargo clippy --workspace -- -D warnings + cargo fmt --all -- --check + +run:: + cargo run -- build --dryrun samples/qemuarm64.yml + +build:: + cargo build + +test:: + cargo test --workspace + +release:: + ( \ + RUSTFLAGS=${STRIP} cargo build --release --locked --target x86_64-unknown-linux-musl; \ + mkdir -p dist; \ + cp target/x86_64-unknown-linux-musl/release/thistle-yocto-build dist/thistle-yocto-build-x86_64-unknown-linux-musl; \ + cd dist; \ + sha256sum thistle-yocto-build-* > sha256sums.txt; \ + ) + +ci:: check-enforce build test + echo "done" + +build-scp:: check release + scp target/x86_64-unknown-linux-musl/release/thistle-yocto-build buildserver:~/ + +docker-prepare:: + docker build -t thistlebuilder . + +docker-build:: + docker run -v ${PWD}/target-ubuntu:/src/target -v ${PWD}:/src -t thistlebuilder + +build-release-images:: build-scp + @echo "Build about to start ! press a key" + @read + scp scripts/build-release-images.sh buildserver:~ + ssh buildserver "bash ~/build-release-images.sh" + scp -r buildserver:~/rpi/dist . + + curl -vvv -X POST --data-binary @dist/zero.img \ + -H "Authorization: Bearer $(shell gcloud auth print-access-token)" \ + "https://storage.googleapis.com/upload/storage/v1/b/thistle-downloads/o?uploadType=media&name=/rpi/zero.img" + + curl -vvv -X POST --data-binary @dist/four.img \ + -H "Authorization: Bearer $(shell gcloud auth print-access-token)" \ + "https://storage.googleapis.com/upload/storage/v1/b/thistle-downloads/o?uploadType=media&name=/rpi/four.img" + + curl -vvv -X POST --data-binary @dist/zero.part \ + -H "Authorization: Bearer $(shell gcloud auth print-access-token)" \ + "https://storage.googleapis.com/upload/storage/v1/b/thistle-downloads/o?uploadType=media&name=/rpi/zero.part" + + curl -vvv -X POST --data-binary @dist/four.part \ + -H "Authorization: Bearer $(shell gcloud auth print-access-token)" \ + "https://storage.googleapis.com/upload/storage/v1/b/thistle-downloads/o?uploadType=media&name=/rpi/four.part" + diff --git a/README.md b/README.md index cebb70c..96b6525 100644 --- a/README.md +++ b/README.md @@ -1 +1,53 @@ -# thistle-yocto-build \ No newline at end of file +# thistle-yocto-build + +This project is designed to wrap around the Yocto build system, to simplify usage and as well as managing common security features over the built image. + +[More information on our docs](https://docs.thistle.tech/thistle_yocto_build/getstarted/qemu) + +Thistle-yocto-build works by accepting a configuration file in YAML format and outputting a fully built image. +The configuration file is based on the format used by the [kas](https://kas.readthedocs.io/en/latest/) project with extensions to specify the security components to be used by the assembled image. + +### Features + +* clone and checkout BitBake layers automatically +* build a Yocto image +* safe defaults (disabled ssh on production builds, etc..) +* default configurations for qemuarm64, Raspberry Pi 4, BeagleBoneBlack +* basic security check & CVE audit post build +* direct integration with Thistle Update Client + +Some features used in the repository are based off the [meta-thistle layer](https://github.com/thistletech/meta-thistle) - such as the Infineon TrustM linux tool integration. + +### Usage + +Dowload a released binary from the [releases page](https://github.com/thistletech/thistle-yocto-build/releases) or see the [build](#build) instructions below. + +```sh +$ ./thistle-yocto-build --help +$ ./thistle-yocto-build gen-config qemu +$ ./thistle-yocto-build build --debug conf.yml +# Default image build time is ~ 45mins on a Ryzen 5 3600. Requires ~50GB of free storage. +``` + +### Build + +_note_: the project only builds on x86_64 linux due to C dependency on [crypt(3) functions](https://github.com/pldubouilh/crypt3-sys) + +```sh +# requires rust, x86_64-unknown-linux-musl target and clang +$ make release +``` + +### Docker Usage + +Build image +```sh +$ docker build -t tyb . +``` + +Run build on docker image +```sh +$ mkdir build +$ cp samples/qemuarm64.yml build/ +$ docker run --rm -v $(pwd)/build:/home/builder tyb thistle-yocto-build qemuarm64.yml build debug +``` diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8b0f3df --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,3 @@ +# Security + +Please report security issues or vulnerabilities to . diff --git a/samples/beagleboneblack.yml b/samples/beagleboneblack.yml new file mode 100644 index 0000000..6f46fa9 --- /dev/null +++ b/samples/beagleboneblack.yml @@ -0,0 +1,42 @@ +header: + version: 11 + +target: core-image-minimal +machine: beaglebone-yocto +distro: poky + +thistle-features: + meta-thistle: e05a0ab0e3abfc3c8fcb5371fdffa451765826af + updater: true + curl: + bin: false + lib: false + tls: openssl + +repos: + poky: + url: https://git.yoctoproject.org/git/poky + refspec: kirkstone + layers: + - meta + - meta-poky + - meta-yocto-bsp + + meta-ti: + url: git://git.yoctoproject.org/meta-ti + refspec: kirkstone + layers: + - meta-ti-bsp + + meta-arm: + url: git://git.yoctoproject.org/meta-arm + refspec: yocto-4.0.1 + layers: + - meta-arm + - meta-arm-toolchain + +local_conf_header: + standard: | + PACKAGE_CLASSES = "package_ipk" + CONF_VERSION = "2" + IMAGE_INSTALL:append = " trust-m trust-m-dev" diff --git a/samples/portenta-x8.yml b/samples/portenta-x8.yml new file mode 100644 index 0000000..29d795c --- /dev/null +++ b/samples/portenta-x8.yml @@ -0,0 +1,78 @@ +header: + version: 11 + +machine: portenta-x8 +target: base +distro: thistle-base + +thistle-features: + meta-thistle: e05a0ab0e3abfc3c8fcb5371fdffa451765826af + curl: + lib: true + bin: true + tls: openssl + +repos: + meta-lmp: + url: https://github.com/thistletech/meta-lmp.git + refspec: f10d8f97cddb539edc63f66e67a62bdcc80a02a7 + layers: + - meta-lmp-base + - meta-lmp-bsp + + meta-openembedded: + url: git://git.openembedded.org/meta-openembedded + refspec: acbe74879807fc6f82b62525d32c823899e19036 + layers: + - meta-oe + - meta-python + - meta-networking + - meta-perl + - meta-filesystems + + meta-security: + url: git://git.yoctoproject.org/meta-security + refspec: c79262a30bd385f5dbb009ef8704a1a01644528e + layers: + - . + - meta-integrity + + meta-updater: + url: https://github.com/lmp-mirrors/meta-updater.git + refspec: 03fc4eb32bccc43f6aa386408dad30205efa5b8e + + meta-virtualization: + url: https://github.com/lgirdk/meta-virtualization.git + refspec: 8c5f038cb92fa4b02246d2d1479a003eecf5fe93 + + openembedded-core: + url: git://git.openembedded.org/openembedded-core + refspec: 54ee67b1a805a07288925d56e9956aabc23b6ab2 + layers: + - meta + + meta-partner-arduino: + url: https://github.com/thistletech/meta-partner-arduino.git + refspec: 3236032b6f4d5ef4b5186d1b5e16644d1ff17821 + + meta-freescale: + url: https://github.com/Freescale/meta-freescale.git + refspec: 2fb1ce365338126aad365012ebb913b3e4a9f1be + + meta-freescale-3rdparty: + url: https://github.com/Freescale/meta-freescale-3rdparty.git + path: meta-freescale-3rdparty + refspec: de0eb1408150d77f9cce97c559f9a5a3c71e5d6c + + meta-arm: + url: git://git.yoctoproject.org/meta-arm + refspec: 0a5eba13d81f5c5722a13b816193ebf93c0fd198 + layers: + - meta-arm-toolchain + +local_conf_header: + accept_fsl_eula: | + ACCEPT_FSL_EULA = "1" + standard: | + PACKAGE_CLASSES = "package_rpm" + IMAGE_INSTALL:append = " lmp-boot-firmware lmp-device-tree " diff --git a/samples/qemuarm64.yml b/samples/qemuarm64.yml new file mode 100644 index 0000000..428a34b --- /dev/null +++ b/samples/qemuarm64.yml @@ -0,0 +1,34 @@ +header: + version: 11 + +target: base +machine: qemuarm64-thistle +distro: thistle-base + +thistle-features: + meta-thistle: e05a0ab0e3abfc3c8fcb5371fdffa451765826af + curl: + bin: true + lib: true + tls: openssl + +repos: + openembedded-core: + url: git://git.openembedded.org/openembedded-core + refspec: 54ee67b1a805a07288925d56e9956aabc23b6ab2 + layers: + - meta + + meta-openembedded: + url: git://git.openembedded.org/meta-openembedded + refspec: kirkstone + layers: + - meta-oe + - meta-python + - meta-networking + - meta-perl + +local_conf_header: + standard: | + PACKAGE_CLASSES = "package_rpm" + CONF_VERSION = "2" \ No newline at end of file diff --git a/samples/raspberrypi4.yml b/samples/raspberrypi4.yml new file mode 100644 index 0000000..d364677 --- /dev/null +++ b/samples/raspberrypi4.yml @@ -0,0 +1,40 @@ +header: + version: 11 + +target: base +machine: raspberrypi4-64-thistle +distro: thistle-base + +thistle-features: + meta-thistle: e05a0ab0e3abfc3c8fcb5371fdffa451765826af + updater: true + curl: + bin: true + lib: true + tls: openssl + +repos: + openembedded-core: + url: git://git.openembedded.org/openembedded-core + refspec: 54ee67b1a805a07288925d56e9956aabc23b6ab2 + layers: + - meta + + meta-raspberry: + url: https://github.com/agherzan/meta-raspberrypi.git + refspec: kirkstone + + meta-openembedded: + url: git://git.openembedded.org/meta-openembedded + refspec: kirkstone + layers: + - meta-oe + - meta-python + - meta-networking + - meta-perl + +local_conf_header: + standard: | + PACKAGE_CLASSES = "package_rpm" + CONF_VERSION = "2" + diff --git a/scripts/build-release-images.sh b/scripts/build-release-images.sh new file mode 100644 index 0000000..69a31a7 --- /dev/null +++ b/scripts/build-release-images.sh @@ -0,0 +1,18 @@ +set -e + +export THISTLE_YOCTO_BUILD_USERNAME=thistle +export THISTLE_YOCTO_BUILD_PASSWORD=raspberry + +mkdir -p ~/rpi +cd rpi +cp ../thistle-yocto-build . + +./thistle-yocto-build clean -y +./thistle-yocto-build gen-config rpi4 +./thistle-yocto-build build -d conf.yml + +rm -rf dist +mkdir dist + +cp build/deploy/images/raspberrypi4-64-thistle/base-raspberrypi4-64-thistle-*.rootfs.wic dist/four.img +cp build/deploy/images/raspberrypi4-64-thistle/base-raspberrypi4-64-thistle-*.rootfs.ext4 dist/four.rootfs \ No newline at end of file diff --git a/src/build.rs b/src/build.rs new file mode 100644 index 0000000..c87c429 --- /dev/null +++ b/src/build.rs @@ -0,0 +1,196 @@ +#![allow(clippy::field_reassign_with_default)] +use crate::config::{Config, BUILD_ENV}; +use crate::credentials::Credentials; +use crate::repo::Repo; +use crate::yocto_configs; +use crate::{log, log_warn}; +use anyhow::{Ok, *}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::process::Command; + +const BITBAKE_REFSPEC: &str = "2022-04.6-kirkstone"; +const BITBAKE_URL: &str = "git://git.openembedded.org/bitbake"; + +const META_THISTLE_URL: &str = "https://github.com/thistletech/meta-thistle.git"; + +pub fn find_bitbake_in_top_layers(layer_abs_paths: Vec) -> Option { + for path in layer_abs_paths { + let path = path.join("bitbake"); + if path.is_dir() { + return Some(path); + } + } + None +} + +fn set_thistle(hash: &str) -> Repo { + let layers = vec![ + "meta-thistle-base".to_string(), + "meta-thistle-base-bsp".to_string(), + "meta-thistle-update-client".to_string(), + "meta-trust-m".to_string(), + ]; + + Repo { + name: "meta-thistle".to_string(), + layers, + location: META_THISTLE_URL.to_string(), + refspec: Some(hash.to_string()), + } +} + +fn find_oe_init() -> Result { + for entry in fs::read_dir(&BUILD_ENV.layer_dir)? { + let path = entry?.path(); + if path.is_dir() { + let oe_init = path.join("oe-init-build-env"); + if oe_init.exists() { + return Ok(oe_init); + } + } + } + bail!("Unable to find oe-init-build-env in any of the layers") +} + +pub fn build( + config_file: &Path, + debug: bool, + dryrun: bool, + bitbake_extra_args: &Option, +) -> Result<()> { + let config = Config::parse_from_path(config_file)?; + let extra_bbargs = bitbake_extra_args.clone().unwrap_or_default(); + let mut repos = config.repos.clone(); + + if debug { + log_warn!("Building in insecure debug mode"); + } + + let dryrun_flag = if dryrun { + log_warn!("running in dry run mode"); + "--dry-run" + } else { + "" + }; + + let creds = match debug { + true => Some(Credentials::new()?), + false => None, + }; + + if let Some(f) = &config.thistle_features { + let thistle_repo = set_thistle(&f.meta_thistle); + repos.push(thistle_repo); + } + + // Fetch repos and parse layer configuration + let (top_layers, layers) = crate::repo::fetch_repos(&repos).context("Unable to fetch repos")?; + + // See if bitbake is already here, or otherwise default to fetching it + if let Some(_bitbake_path) = find_bitbake_in_top_layers(top_layers) { + log!("Found bitbake on top layer"); + } else { + log!("Fetching bitbake from upstream git repo at commit {}", &BITBAKE_REFSPEC); + let r = Repo { + name: "bitbake".to_string(), + layers: Vec::new(), + location: BITBAKE_URL.to_string(), + refspec: Some(BITBAKE_REFSPEC.to_string()), + }; + r.fetch().context("unable to fetch bitbake")?; + } + + let oe = find_oe_init()?; + log!("Found oe-init-build-env at {:?}", oe); + + // Create configuration files + log!("Generating bitbake config files"); + yocto_configs::local_conf_write(&config, debug, creds).context("unable to write conf file")?; + yocto_configs::bblayer_conf_write(&config, layers).context("unable to write conf file")?; + yocto_configs::site_conf_write().context("unable to write conf file")?; + + let build_script_path = "./.thistlebuild.sh"; + let source_oe = format!("source {oe:?} > /dev/null"); + let bb_cmd = format!("bitbake {} {} -k {}", extra_bbargs, dryrun_flag, config.target); + let exec_script = format!("{} && {}", source_oe, bb_cmd); + + log!("Executing {bb_cmd:?}"); + log!("\nBuild starting - {:?}\n", chrono::offset::Local::now()); + let ts = chrono::offset::Local::now().timestamp(); + + fs::write(build_script_path, exec_script).context("can't write build file")?; + let build_err = "can't run build script"; + + let cmd = Command::new("bash") + .arg(build_script_path) + .spawn() + .context(build_err)? + .wait() + .context(build_err)?; + + let ts_end = chrono::offset::Local::now().timestamp(); + + if dryrun { + log!("\nDry run done in {}s", ts_end - ts); + return Ok(()); + } + + log!("\nBuild done in {}s", ts_end - ts); + + if !cmd.success() { + crate::common::notify_user("Build failed", "error"); + return Ok(()); + } + + crate::common::notify_user("Build successful", "information"); + log!("Build artifact folder: {:?}", BUILD_ENV.deploy_dir); + + if let Err(e) = check_for_insecure_features(&config) { + log_warn!("Failed scanning build for insecure features"); + log!("{:?}", e); + } + + Ok(()) +} + +fn check_for_insecure_features(config: &Config) -> Result<()> { + log!("Analyzing build for security issues..."); + + let test_data_path = format!( + "build/deploy/images/{}/{}-{}.testdata.json", + &config.machine, &config.target, &config.machine + ); + + let td = std::fs::read_to_string(test_data_path).context("failed to locate build metatada")?; + let test_data_json: serde_json::Value = serde_json::from_str(&td)?; + let test_data_map = test_data_json.as_object().ok_or(anyhow!("unable to parse local data"))?; + + let image_features = + test_data_map.get("IMAGE_FEATURES").ok_or(anyhow!("unable to find image features"))?; + + let image_features: Vec<&str> = match image_features { + serde_json::Value::String(s) => s.split_ascii_whitespace().collect(), + _ => panic!("Unsupported json type"), + }; + + let insecure_image_features = vec![ + "allow-empty-password", + "debug-tweaks", + "empty-root-password", + "post-install-logging", + ]; + + let mut insecure_image_feature_found = false; + for insecure_image_feature in insecure_image_features { + if image_features.contains(&insecure_image_feature) { + log_warn!("Found insecure image feature: {}", &insecure_image_feature); + insecure_image_feature_found = true; + } + } + + if !insecure_image_feature_found { + log!("No issues detected"); + } + Ok(()) +} diff --git a/src/common.rs b/src/common.rs new file mode 100644 index 0000000..0a667cf --- /dev/null +++ b/src/common.rs @@ -0,0 +1,30 @@ +use std::process::Command; + +#[macro_export] +macro_rules! log_warn { + ($x:expr) => ( + { eprint!("\x1b[93mWARNING: "); eprint!($x); eprintln!("\x1b[0m")} + ); + ($x:expr, $($more:expr),+) => ( + { eprint!("\x1b[93mWARNING: "); eprint!($x, $($more),*); eprintln!("\x1b[0m")} + ); +} +#[macro_export] +macro_rules! log { + ($x:expr) => ( + { eprint!(""); eprintln!($x); } + ); + ($x:expr, $($more:expr),+) => ( + { eprint!(""); eprintln!($x, $($more),*); } + ); +} + +pub fn notify_user(msg: &str, icon: &str) { + log_warn!("{msg}"); + + let icon = "--icon=dialog-".to_string() + icon; + + let _cmd = Command::new("notify-send").arg(&icon).arg("Thistle Yocto Build").arg(msg).output(); + + // no err handling - best effort (not available headless, etc...) +} diff --git a/src/config/common.rs b/src/config/common.rs new file mode 100644 index 0000000..af79b50 --- /dev/null +++ b/src/config/common.rs @@ -0,0 +1,22 @@ +// Handle custom deserialization errors +macro_rules! bail_de { + ($msg:literal $(,)?) => { + { + use serde::de::Error; + return Err(serde_yaml::Error::custom($msg)) + } + }; + ($err:expr $(,)?) => { + { + use serde::de::Error; + return Err(serde_yaml::Error::custom($err)) + } + }; + ($fmt:expr, $($arg:tt)*) => { + { + use serde::de::Error; + let msg = format!($fmt, $($arg)*); + return Err(serde_yaml::Error::custom(msg)) + } + }; +} diff --git a/src/config/error.rs b/src/config/error.rs new file mode 100644 index 0000000..9822eb6 --- /dev/null +++ b/src/config/error.rs @@ -0,0 +1,9 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("IO error")] + Io(#[from] std::io::Error), + #[error("Parse error")] + Parse(#[from] serde_yaml::Error), +} diff --git a/src/config/features/curl.rs b/src/config/features/curl.rs new file mode 100644 index 0000000..4f291cc --- /dev/null +++ b/src/config/features/curl.rs @@ -0,0 +1,110 @@ +use crate::config::features::TLS; +use serde::de::Error; +use serde::{Deserialize, Serialize}; +use serde_yaml::Value; +use std::str::FromStr; + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Curl { + pub bin: bool, + pub lib: bool, + pub tls: TLS, +} + +impl TryFrom<&Value> for Curl { + type Error = serde_yaml::Error; + fn try_from(v: &Value) -> Result { + let map = v + .as_sequence() + .unwrap() + .first() + .unwrap() + .as_mapping() + .unwrap() + .get(&Value::String("curl".to_string())) + .unwrap() + .as_mapping() + .unwrap(); + let mut lib = false; + let mut bin = false; + let mut tls = None; + for (k, v) in map.iter() { + let k = k.as_str().ok_or_else(|| { + serde_yaml::Error::custom("Unexpected field type for 'curl'. Must be a string.") + })?; + match k { + "lib" => { + lib = v.as_bool().ok_or_else(|| { + serde_yaml::Error::custom("Unexpected field type for 'curl' lib. Must be a bool.") + })?; + } + "bin" => { + bin = v.as_bool().ok_or_else(|| { + serde_yaml::Error::custom("Unexpected field type for 'curl' bin. Must be a bool.") + })?; + } + "tls" => { + let tls_str = v.as_str().ok_or_else(|| { + serde_yaml::Error::custom("Unexpected field type for 'curl' tls. Must be a string.") + })?; + tls = Some(TLS::from_str(tls_str)?); + } + _ => { + bail_de!("Unsupported key '{}' found for 'curl'", k); + } + } + } + + if !lib && !bin { + bail_de!("Attempting to include 'curl' without bin or lib flags set"); + } + + let tls = match tls { + Some(TLS::BearSSL) | Some(TLS::WolfSSL) => { + bail_de!("The selected TLS stack is not supported by 'curl'"); + } + Some(t) => t, + None => bail_de!("A TLS stack must be selected for 'curl'"), + }; + + let curl_imp = Curl { bin, lib, tls }; + Ok(curl_imp) + } +} + +impl Curl { + pub fn to_image_install_value(&self) -> String { + let mut ret = "IMAGE_INSTALL:append = \" ".to_string(); + if self.bin { + ret.push_str("curl"); + } + if self.lib { + ret.push_str(" libcurl"); + } + + ret.push_str("\"\n"); + ret.push_str(&format!("PACKAGECONFIG:append:pn-curl = \" {}\"", self.tls.to_image_install_value())); + ret + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_normal_config() { + let example = r#" + - curl: + lib: true + bin: true + tls: "rustls" + "#; + let example_yaml: serde_yaml::Value = serde_yaml::from_str(example).unwrap(); + let curl = Curl::try_from(&example_yaml).unwrap(); + let expected_output = "IMAGE_INSTALL:append = \" curl libcurl\"\n\ + PACKAGECONFIG:append:pn-curl = \" rustls\""; + let value = curl.to_image_install_value(); + assert_eq!(&value, expected_output); + } +} diff --git a/src/config/features/libc.rs b/src/config/features/libc.rs new file mode 100644 index 0000000..2a850a4 --- /dev/null +++ b/src/config/features/libc.rs @@ -0,0 +1,38 @@ +use serde::{Deserialize, Serialize}; +use serde_yaml::Value; +use std::str::FromStr; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum Libc { + Musl, + Glibc, +} + +impl Default for Libc { + fn default() -> Self { + Self::Glibc + } +} + +impl FromStr for Libc { + type Err = serde_yaml::Error; + + fn from_str(value: &str) -> Result { + let result = match value { + "musl" => Self::Musl, + "glibc" => Self::Glibc, + _ => bail_de!("Invalid libc type: {}", value), + }; + Ok(result) + } +} + +impl Libc { + pub fn parse(v: &Value) -> Result { + match v { + Value::String(v) => Self::from_str(v), + _ => bail_de!("Unexpected field type for 'libc'"), + } + } +} diff --git a/src/config/features/mod.rs b/src/config/features/mod.rs new file mode 100644 index 0000000..ad28993 --- /dev/null +++ b/src/config/features/mod.rs @@ -0,0 +1,47 @@ +use std::io::Write; + +pub mod curl; +pub mod libc; +pub mod tls; + +use crate::config::features::libc::Libc; +use serde::{Deserialize, Serialize}; +use tls::TLS; + +use self::curl::Curl; + +impl Features { + pub fn write_local_conf(&self, w: &mut Vec) -> Result<(), std::io::Error> { + if self.updater { + writeln!(w, "IMAGE_INSTALL:append = \" embedded-client\"")?; + } + + if self.read_only_rootfs { + writeln!(w, "IMAGE_FEATURES:append = \" read-only-rootfs\"")?; + } + + if let Some(curl) = &self.curl { + writeln!(w, "{}", curl.to_image_install_value())?; + } + + match self.libc { + Some(Libc::Musl) => writeln!(w, "TCLIBC = \"musl\"")?, + Some(Libc::Glibc) => writeln!(w, "TCLIBC = \"glibc\"")?, + None => (), + } + Ok(()) + } +} + +#[derive(Default, Clone, Debug, Serialize, Deserialize)] +pub struct Features { + #[serde(rename = "meta-thistle")] + pub meta_thistle: String, + #[serde(default)] + #[serde(rename = "read-only-rootfs")] + pub read_only_rootfs: bool, + #[serde(default)] + pub updater: bool, + pub libc: Option, + pub curl: Option, +} diff --git a/src/config/features/tls.rs b/src/config/features/tls.rs new file mode 100644 index 0000000..cf3889a --- /dev/null +++ b/src/config/features/tls.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Serialize}; +use std::str::FromStr; + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum TLS { + BearSSL, + WolfSSL, + OpenSSL, + GnuTLS, + MbedTLS, + RusTLS, +} + +impl FromStr for TLS { + type Err = serde_yaml::Error; + + fn from_str(value: &str) -> Result { + let result = match value { + "bearssl" => Self::BearSSL, + "wolfssl" => Self::WolfSSL, + "openssl" => Self::OpenSSL, + "gnutls" => Self::GnuTLS, + "mbedtls" => Self::MbedTLS, + "rustls" => Self::RusTLS, + _ => bail_de!("Invalid tls type: {}", value), + }; + Ok(result) + } +} + +impl TLS { + pub(super) fn to_image_install_value(&self) -> String { + match self { + Self::BearSSL => "bearssl", + Self::WolfSSL => "wolfssl", + Self::OpenSSL => "openssl", + Self::GnuTLS => "gnutls", + Self::MbedTLS => "mbedtls", + Self::RusTLS => "rustls", + } + .to_string() + } +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..4869a31 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,94 @@ +#[macro_use] +mod common; +pub mod error; +pub mod features; + +use anyhow::*; +use serde::Deserialize; +use std::fmt::Debug; +use std::path::Path; +use std::{collections::BTreeMap, path::PathBuf}; + +use crate::repo::Repo; + +use self::features::Features; + +pub struct BuildEnv { + pub work_dir: PathBuf, + pub layer_dir: PathBuf, + pub deploy_dir: PathBuf, + pub conf_dir: PathBuf, +} + +lazy_static! { + pub static ref BUILD_ENV: BuildEnv = { + let work_dir = PathBuf::from("./build"); + std::fs::create_dir_all(&work_dir).expect("can't handle directory ./build, check permissions"); + let work_dir = work_dir.canonicalize().expect("can't handle directory ./build, check permissions"); + + let prepare_path = |t: &str| -> PathBuf { + let err = format!("can't handle directory ./build/{t}, check permissions"); + let p = work_dir.join(t); + std::fs::create_dir_all(&p).expect(&err); + p.canonicalize().expect(&err) + }; + + let layer_dir = prepare_path("layers"); + let deploy_dir = prepare_path("deploy"); + let conf_dir = prepare_path("conf"); + + BuildEnv { + work_dir, + layer_dir, + deploy_dir, + conf_dir, + } + }; +} + +impl Config { + pub fn parse_from_path(config_path: &Path) -> Result { + let cantfind = "Unable to find config file path"; + let config_file_path = std::fs::canonicalize(config_path).context(cantfind)?; + let config_file = std::fs::File::open(config_file_path).context(cantfind)?; + + let cantparse = "Unable to parse config file"; + let record: Self = serde_yaml::from_reader(config_file).context(cantparse)?; + Ok(record) + } +} + +fn parse_repos<'de, D>(deserializer: D) -> std::result::Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let mut repos: Vec = Vec::new(); + let map = BTreeMap::::deserialize(deserializer)?; + + for (name, mut repo) in map { + repo.name = name; + repos.push(repo); + } + + std::result::Result::Ok(repos) +} + +#[derive(Debug, Deserialize)] +pub struct Config { + pub target: String, + pub machine: String, + pub distro: String, + + #[serde(rename = "thistle-features")] + pub thistle_features: Option, + pub ccache: Option, + + // deserialize this with a custom deserializer named bobdylan + #[serde(deserialize_with = "parse_repos")] + pub repos: Vec, + + #[serde(rename = "local_conf_header")] + pub local_conf: Option>, + #[serde(rename = "bblayers_conf_header")] + pub bblayers_conf: Option>, +} diff --git a/src/credentials.rs b/src/credentials.rs new file mode 100644 index 0000000..ce0a92d --- /dev/null +++ b/src/credentials.rs @@ -0,0 +1,53 @@ +use std::io::Write; + +use anyhow::*; +use rand::{distributions::Alphanumeric, thread_rng, Rng}; +use rpassword::read_password; + +pub struct Credentials { + pub username: String, + pub password: String, + pub salt: String, +} + +fn get_thistle_yocto_build_env_var(name: &str, private: bool) -> Result { + let name_uc_underscore = name.to_uppercase().replace(' ', "_"); + let key = format!("THISTLE_YOCTO_BUILD_{name_uc_underscore}"); + + let value = std::env::var(&key).or_else(|_| { + print!("The {key} environment variable was not set. Please enter a value for the {name}: "); + std::io::stdout().flush()?; + + let r = if private { + read_password()? + } else { + let mut value = String::new(); + std::io::stdin().read_line(&mut value)?; + value.trim().to_string() + }; + + Ok(r) + })?; + + Ok(value) +} + +impl Credentials { + pub fn new() -> Result { + let username = get_thistle_yocto_build_env_var("username", false)?; + let password = get_thistle_yocto_build_env_var("password", true)?; + + // use env salt if we have some + let salt = std::env::var("THISTLE_YOCTO_BUILD_SALT").unwrap_or_else(|_| { + let mut rng = thread_rng(); + let salt: String = (0..16).map(|_| rng.sample(Alphanumeric) as char).collect(); + salt + }); + + Ok(Self { + username, + password, + salt, + }) + } +} diff --git a/src/crypt3.rs b/src/crypt3.rs new file mode 100644 index 0000000..e1069bb --- /dev/null +++ b/src/crypt3.rs @@ -0,0 +1,22 @@ +use std::ffi::{CStr, CString}; + +use anyhow::*; + +pub fn crypt3_sha256_escaped(password: String, salt: String) -> Result { + let sha256_setting = "$5$"; + let settings = format!("{sha256_setting}{salt}"); + + let csetting = CString::new(settings).context("invalid password")?; + let cpassword = CString::new(password).context("invalid salt")?; + + let mut output = vec![0_i8; 256]; + + let ret_cstr = unsafe { + let _ret = crypt3_sys::crypt_r(cpassword.as_ptr(), csetting.as_ptr(), output.as_mut_ptr()); + CStr::from_ptr(output.as_ptr()) + }; + + let ret_str = ret_cstr.to_str().context("can not parse hashed password")?.to_string(); + let escaped = ret_str.replace('$', "\\$"); + Ok(escaped) +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b041077 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +#[macro_use] +extern crate lazy_static; +pub mod build; +pub mod common; +pub mod config; +pub mod credentials; +pub mod crypt3; +pub mod repo; +pub mod yocto_configs; diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c7f94c9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,158 @@ +use clap::{Parser, Subcommand}; +use std::io::Write; +use std::path::PathBuf; + +use anyhow::*; +use thistle_yocto_build::log; + +#[derive(Parser, Debug)] +#[clap( + name = "thistle yocto build", + author, + version, + disable_help_subcommand = true, + about = "Helper tool for yocto build system.\n +Examples: + Initialize default config: ./thistle-yocto-build gen-config qemu + Start build: ./thistle-yocto-build build conf.yml +", + verbatim_doc_comment, + // term_width=110, +)] +pub struct Args { + #[clap(subcommand)] + pub command: Commands, +} + +#[derive(Debug, Subcommand)] +pub enum Commands { + #[clap(about = "Perform a yocto build")] + Build(BuildArgs), + #[clap( + about = "Clean the build directory. Preserves sscache, and optionally the layers and downloads folders." + )] + Clean(CleanArgs), + #[clap(subcommand, about = "Generate default configuration files")] + GenConfig(ConfArg), +} + +#[derive(Debug, Parser, Default)] +pub struct CleanArgs { + /// Do not prompt for confirmation + #[clap(short, long)] + pub yes: bool, + + /// Preserve layers folder + #[clap(short, long)] + pub layers_keep: bool, + + /// Preserve downloads folder + #[clap(short, long)] + pub dl_keep: bool, +} + +#[derive(Debug, Subcommand, PartialEq)] +pub enum ConfArg { + /// qemu default config + Qemu, + /// raspberrypi 4 default config + Rpi4, + /// beagleboneblack default config + Bbb, +} + +#[derive(Debug, Parser, Default)] +pub struct BuildArgs { + /// Extra arguments to pass to bitbake + #[clap(short, long)] + pub bitbake_extra_args: Option, + + /// Enable debug features (insecure) + #[clap(short, long)] + pub debug: bool, + + /// Download assets but stop before performing the yocto build + #[clap(long)] + pub dryrun: bool, + + /// Configuration file to use + pub config_file: PathBuf, +} + +pub fn run_build(args: BuildArgs) -> Result<()> { + log!("~~ Thistle Yocto Build Starting ~~"); + use thistle_yocto_build::build::build; + build(&args.config_file, args.debug, args.dryrun, &args.bitbake_extra_args)?; + Ok(()) +} + +fn clean(c: CleanArgs) -> Result<()> { + if !c.yes { + let mut empty = "".to_string(); + log!("WARNING: this will clean the build directory. Press any key to continue."); + log!("NOTE: sstate-cache folder will be preserved"); + if c.layers_keep { + log!("NOTE: layers folder will be preserved"); + } + if c.dl_keep { + log!("NOTE: downloads folder will be preserved"); + } + std::io::stdin().read_line(&mut empty)?; + } + + std::fs::rename("build", ".thistletmpbuild").context("build directory does not exist")?; + std::fs::create_dir("build")?; + + std::fs::rename(".thistletmpbuild/sstate-cache", "build/sstate-cache")?; + if c.layers_keep { + std::fs::rename(".thistletmpbuild/layers", "build/layers")?; + } + if c.dl_keep { + std::fs::rename(".thistletmpbuild/downloads", "build/downloads")?; + } + + std::fs::remove_dir_all(".thistletmpbuild")?; + + log!("Cleanup done!"); + Ok(()) +} + +fn gen_config(c: ConfArg) -> Result<()> { + let config_str = match c { + ConfArg::Qemu => include_str!("../samples/qemuarm64.yml"), + ConfArg::Rpi4 => include_str!("../samples/raspberrypi4.yml"), + ConfArg::Bbb => include_str!("../samples/beagleboneblack.yml"), + }; + + let mut config_file = std::fs::File::create("conf.yml").context("Unable to create config file")?; + config_file.write_all(config_str.as_bytes()).context("Unable to write config file")?; + + log!("Generated default thistle-yocto-build config file at conf.yml"); + log!("you can directly build this configuration by running './thistle-yocto-build build conf.yml'"); + + if c == ConfArg::Qemu { + log!("This qemu image can be emulated directly on your system using the following command (requres qemu-system-aarch64)"); + log!( + r#" +qemu-system-aarch64 -machine virt -nographic -cpu cortex-a57 \ + -bios build/deploy/images/qemuarm64-thistle/u-boot.bin \ + -drive if=none,file=build/deploy/images/qemuarm64-thistle/base-qemuarm64-thistle.wic,id=disk0,format=raw \ + -device ich9-ahci,id=ahci -device ide-hd,drive=disk0,bus=ahci.0 -m 256M +"# + ); + } + + Ok(()) +} + +pub fn main() -> Result<()> { + let args = Args::parse(); + + match args.command { + Commands::Build(b) => run_build(b)?, + Commands::Clean(c) => clean(c)?, + Commands::GenConfig(c) => gen_config(c)?, + } + + Ok(()) +} diff --git a/src/repo.rs b/src/repo.rs new file mode 100644 index 0000000..77747d6 --- /dev/null +++ b/src/repo.rs @@ -0,0 +1,110 @@ +use crate::config::BUILD_ENV; +use crate::{log, log_warn}; +use anyhow::*; +use serde::Deserialize; +use std::path::PathBuf; +use std::process::Command; + +#[derive(Debug, Deserialize, Default, Clone)] +pub struct Repo { + #[serde(skip)] + pub name: String, + #[serde(default)] + pub layers: Vec, + + #[serde(alias = "url", alias = "path")] + pub location: String, + + // git only + pub refspec: Option, +} + +fn rungit(path: &PathBuf, args: &[&str]) -> Result { + let c = Command::new("git") + .current_dir(path) + .args(args) + .output() + .context("Failed to run git command")?; + let stdout = String::from_utf8_lossy(&c.stdout).trim().to_string(); + if !c.status.success() { + eprintln!("stdout: {}", stdout); + eprintln!("stderr: {}", String::from_utf8_lossy(&c.stderr)); + bail!("Failed to run git command"); + } + Ok(stdout) +} + +impl Repo { + pub fn is_local_repo(&self) -> bool { + PathBuf::from(&self.location).exists() + } + + pub fn path(&self) -> PathBuf { + let p = if self.is_local_repo() { + // local repo - use path directly + PathBuf::from(&self.location) + } else { + // we buffer the repote repo in the layers folder + BUILD_ENV.layer_dir.join(&self.name) + }; + + p.canonicalize().unwrap_or(p) + } + + pub fn fetch(&self) -> Result<()> { + if self.is_local_repo() { + log!("Scanning local repository {}", self.name); + return Ok(()); // local repo + } + + if self.refspec.is_none() { + bail!("Missing refspec for repository: {}", self.name) + } + + let refspec = self.refspec.clone().unwrap_or_default(); + let url = self.location.clone(); + let path = self.path(); + + if !path.exists() { + log!("Cloning remote repo {}", self.name); + let path_str = path.to_string_lossy().to_string(); + std::fs::create_dir_all(&path).context("can't create repo folder")?; + rungit(&path, &["clone", &url, &path_str])?; + } else { + let repo_not_tainted = rungit(&path, &["status", "--porcelain"])?.is_empty(); + if !repo_not_tainted { + log_warn!("Repository {} is locally changed, not checking out ref {}", self.name, &refspec); + return Ok(()); + } + + log!("Updating remote repo {}", self.name); + let main = rungit(&path, &["symbolic-ref", "--short", "refs/remotes/origin/HEAD"])?; + let main = main.replace("origin/", ""); + rungit(&path, &["checkout", &main])?; + rungit(&path, &["pull"])?; + } + + rungit(&path, &["checkout", &refspec])?; + Ok(()) + } +} + +pub fn fetch_repos(repos: &Vec) -> Result<(Vec, Vec)> { + let mut top_layers = Vec::new(); + let mut layers = Vec::new(); + for r in repos { + r.fetch()?; + top_layers.push(r.path()); + + if r.layers.is_empty() { + layers.push(r.path()); + } else { + for layer in &r.layers { + let layer_path = r.path().join(layer); + layers.push(layer_path); + } + } + } + + Ok((top_layers, layers)) +} diff --git a/src/yocto_configs.rs b/src/yocto_configs.rs new file mode 100644 index 0000000..9877f9a --- /dev/null +++ b/src/yocto_configs.rs @@ -0,0 +1,128 @@ +use anyhow::*; +use std::{fs::File, io::Write, path::PathBuf}; + +use crate::{ + config::{Config, BUILD_ENV}, + credentials::Credentials, + crypt3::crypt3_sha256_escaped, + log, +}; + +// local.conf +pub fn local_conf_write(conf: &Config, debug: bool, c: Option) -> Result<()> { + let p = BUILD_ENV.conf_dir.join("local.conf"); + let mut file = File::create(p)?; + let mut w = Vec::new(); + writeln!(w, "MACHINE ??= \"{}\"", conf.machine)?; + writeln!(w, "DISTRO ??= \"{}\"", conf.distro)?; + writeln!(w, "SDKMACHINE = \"x86_64\"")?; + writeln!(w)?; + + // Enable build history which outputs all sorts of interesting build info such as the dependency graph and a complete file manifest + writeln!(w, "INHERIT += \"buildhistory\"")?; + writeln!(w, "BUILDHISTORY_COMMIT = \"0\"")?; + writeln!(w, "BUILDHISTORY_FEATURES = \"image\"")?; + writeln!(w)?; + + if let Some(tf) = &conf.thistle_features { + tf.write_local_conf(&mut w)?; + } + + if debug { + writeln!(w, "IMAGE_FEATURES:append = \" ssh-server-openssh\"")?; + writeln!(w, "IMAGE_INSTALL:append = \" ssh-pregen-hostkeys\"")?; + + if let Some(creds) = &c { + let username = &creds.username; + let password = creds.password.clone(); + let salt = creds.salt.clone(); + let passwd = crypt3_sha256_escaped(password, salt).unwrap(); + + writeln!(w, "INHERIT += \"extrausers\"")?; + writeln!(w, "EXTRA_USERS_PARAMS = \"useradd -p '{passwd}' -G sudo {username};\"")?; + } + } + + if conf.ccache.unwrap_or(true) { + log!("Enabling ccache"); + writeln!(w, "INHERIT += \"ccache\"")?; + } + + writeln!(w, "IMAGE_INSTALL:append = \" sudo\"")?; + + // // enable CVE checking on non debug build + // if !self.build_options.debug { + // writeln!(w, "INHERIT += \"cve-check\"")?; + // } + + log!("Setting up disk monitoring system "); + + let monitoring = r#"# disk monitoring +BB_DISKMON_DIRS = "\ + STOPTASKS,${TMPDIR},1G,100K \ + STOPTASKS,${DL_DIR},1G,100K \ + STOPTASKS,${SSTATE_DIR},1G,100K \ + STOPTASKS,/tmp,100M,100K""#; + + writeln!(w, "{monitoring}")?; + + // auto detect CPUS + let cpus = num_cpus::get(); + log!("Detected {} CPUs, will configure build accordingly ", cpus); + + writeln!(w, "BB_NUMBER_THREADS = \"{cpus}\"")?; + writeln!(w, "PARALLEL_MAKE = \"-j {cpus}\"")?; + + // add all custom fields from local_conf + if let Some(conf) = &conf.local_conf { + for (k, v) in conf { + writeln!(w, "# {k}")?; + writeln!(w, "{v}")?; + } + } + + file.write_all(w.as_slice())?; + file.flush()?; + Ok(()) +} + +// bblayers.conf +pub fn bblayer_conf_write(conf: &Config, layers: Vec) -> Result<()> { + let p = BUILD_ENV.conf_dir.join("bblayers.conf"); + let mut file = File::create(p)?; + let mut w = Vec::new(); + + if let Some(conf) = &conf.bblayers_conf { + for (k, v) in conf { + writeln!(w, "# {k}")?; + writeln!(w, "{v}")?; + } + } + + let layers: Vec = layers.iter().map(|l| l.to_string_lossy().to_string()).collect(); + let layers_str = layers.join(" \\\n "); + + write!(w, "BBLAYERS ?= \" \\\n {layers_str}")?; + writeln!(w, "\"")?; + + writeln!(w, "BBPATH ?= \"${{TOPDIR}}\"")?; + writeln!(w, "BBFILES ??= \"\"")?; + + file.write_all(w.as_slice())?; + file.flush()?; + Ok(()) +} + +// site.conf +pub fn site_conf_write() -> Result<()> { + let p = BUILD_ENV.conf_dir.join("site.conf"); + let mut file = File::create(p)?; + let mut w = Vec::new(); + + writeln!(w, "SCONF_VERSION = \"1\"")?; + writeln!(w, "DEPLOY_DIR = \"{}\"", BUILD_ENV.deploy_dir.to_string_lossy())?; + + file.write_all(w.as_slice())?; + file.flush()?; + Ok(()) +}