From ddc990b522aa3c45a1ef8e2475451c2580115f22 Mon Sep 17 00:00:00 2001 From: Dominic Date: Wed, 5 Apr 2023 14:05:11 +0200 Subject: [PATCH] initial commit --- .gitignore | 20 ++ Cargo.lock | 653 ++++++++++++++++++++++++++++++++++ Cargo.toml | 11 + LICENSE | 277 ++++++++++++++ rustfmt.toml | 35 ++ src/main.rs | 23 ++ src/steps/init_first_boot.rs | 47 +++ src/steps/init_network.rs | 111 ++++++ src/steps/init_os.rs | 189 ++++++++++ src/steps/install_packages.rs | 52 +++ src/steps/mod.rs | 183 ++++++++++ src/steps/prepare_img.rs | 57 +++ src/steps/set_passwords.rs | 21 ++ 13 files changed, 1679 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 rustfmt.toml create mode 100644 src/main.rs create mode 100644 src/steps/init_first_boot.rs create mode 100644 src/steps/init_network.rs create mode 100644 src/steps/init_os.rs create mode 100644 src/steps/install_packages.rs create mode 100644 src/steps/mod.rs create mode 100644 src/steps/prepare_img.rs create mode 100644 src/steps/set_passwords.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4c4313f --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# sorry but I don't need my network config public +setup/network.toml +# also other config contains like access tokens so just hide everything +setup/ + +# rust/cargo +/target/ + +# git +*.orig + +# emacs +*~ +\#*# +.#*# + +# intellij +/.idea/ +*.ipr +*.iml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..376a2c7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,653 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + +[[package]] +name = "attohttpc" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9a9bf8b79a749ee0b911b91b671cc2b6c670bdbc7e3dfd537576ddc94bb2a2" +dependencies = [ + "flate2", + "http", + "log", + "native-tls", + "url", +] + +[[package]] +name = "basic-toml" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c0de75129aa8d0cceaf750b89013f0e08804d6ec61416da787b35ad0d7cddf1" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "errno" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d6a0976c999d473fe89ad888d5a284e55366d9dc9038b1ba2aa15128c4afa0" +dependencies = [ + "errno-dragonfly", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "flate2" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a2db397cb1c8772f31494cb8917e48cd1e64f0fa7efac59fbd741a0a8ce841" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[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 = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "http" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[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 = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c66c74d2ae7e79a5a8f7ac924adbe38ee42a859c6539ad869eb51f0b52dc220" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.141" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" + +[[package]] +name = "linux-raw-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "mkalpiimg" +version = "0.1.0" +dependencies = [ + "anyhow", + "attohttpc", + "basic-toml", + "serde", + "tempfile", +] + +[[package]] +name = "native-tls" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +dependencies = [ + "lazy_static", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "openssl" +version = "0.10.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d2f106ab837a24e03672c59b1239669a0596406ff657c3c0835b6b7f0f35a33" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-sys" +version = "0.9.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a20eace9dc2d82904039cb76dcf50fb1a0bba071cfd1629720b5d6f1ddba0fa" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "proc-macro2" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "rustix" +version = "0.37.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aae838e49b3d63e9274e1c01833cc8139d3fec468c3b84688c628f44b1ae11d" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "schannel" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "713cfb06c7059f3588fb8044c0fad1d09e3c01d225e25b9220dbfdcf16dbb1b3" +dependencies = [ + "windows-sys 0.42.0", +] + +[[package]] +name = "security-framework" +version = "2.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a332be01508d814fed64bf28f798a146d73792121129962fdf335bb3c49a4254" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31c9bb296072e961fcbd8853511dd39c2d8be2deb1e17c6860b1d30732b323b4" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "2.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c9da457c5285ac1f936ebd076af6dac17a61cfe7826f2076b4d015cf47bc8ec" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9fbec84f381d5795b08656e4912bec604d162bff9291d6189a78f4c8ab87998" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall", + "rustix", + "windows-sys 0.45.0", +] + +[[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.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[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 = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5" +dependencies = [ + "windows_aarch64_gnullvm 0.48.0", + "windows_aarch64_msvc 0.48.0", + "windows_i686_gnu 0.48.0", + "windows_i686_msvc 0.48.0", + "windows_x86_64_gnu 0.48.0", + "windows_x86_64_gnullvm 0.48.0", + "windows_x86_64_msvc 0.48.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5bdbbfd --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mkalpiimg" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0" +attohttpc = "0.24" +serde = { version = "1.0", features = ["derive"] } +tempfile = "3.4" +toml = { version = "0.1", package = "basic-toml" } diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d3087e4 --- /dev/null +++ b/LICENSE @@ -0,0 +1,277 @@ +Eclipse Public License - v 2.0 + + THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE + PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION + OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS + +"Contribution" means: + + a) in the case of the initial Contributor, the initial content + Distributed under this Agreement, and + + b) in the case of each subsequent Contributor: + i) changes to the Program, and + ii) additions to the Program; + where such changes and/or additions to the Program originate from + and are Distributed by that particular Contributor. A Contribution + "originates" from a Contributor if it was added to the Program by + such Contributor itself or anyone acting on such Contributor's behalf. + Contributions do not include changes or additions to the Program that + are not Modified Works. + +"Contributor" means any person or entity that Distributes the Program. + +"Licensed Patents" mean patent claims licensable by a Contributor which +are necessarily infringed by the use or sale of its Contribution alone +or when combined with the Program. + +"Program" means the Contributions Distributed in accordance with this +Agreement. + +"Recipient" means anyone who receives the Program under this Agreement +or any Secondary License (as applicable), including Contributors. + +"Derivative Works" shall mean any work, whether in Source Code or other +form, that is based on (or derived from) the Program and for which the +editorial revisions, annotations, elaborations, or other modifications +represent, as a whole, an original work of authorship. + +"Modified Works" shall mean any work in Source Code or other form that +results from an addition to, deletion from, or modification of the +contents of the Program, including, for purposes of clarity any new file +in Source Code form that contains any contents of the Program. Modified +Works shall not include works that contain only declarations, +interfaces, types, classes, structures, or files of the Program solely +in each case in order to link to, bind by name, or subclass the Program +or Modified Works thereof. + +"Distribute" means the acts of a) distributing or b) making available +in any manner that enables the transfer of a copy. + +"Source Code" means the form of a Program preferred for making +modifications, including but not limited to software source code, +documentation source, and configuration files. + +"Secondary License" means either the GNU General Public License, +Version 2.0, or any later versions of that license, including any +exceptions or additional permissions as identified by the initial +Contributor. + +2. GRANT OF RIGHTS + + a) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free copyright + license to reproduce, prepare Derivative Works of, publicly display, + publicly perform, Distribute and sublicense the Contribution of such + Contributor, if any, and such Derivative Works. + + b) Subject to the terms of this Agreement, each Contributor hereby + grants Recipient a non-exclusive, worldwide, royalty-free patent + license under Licensed Patents to make, use, sell, offer to sell, + import and otherwise transfer the Contribution of such Contributor, + if any, in Source Code or other form. This patent license shall + apply to the combination of the Contribution and the Program if, at + the time the Contribution is added by the Contributor, such addition + of the Contribution causes such combination to be covered by the + Licensed Patents. The patent license shall not apply to any other + combinations which include the Contribution. No hardware per se is + licensed hereunder. + + c) Recipient understands that although each Contributor grants the + licenses to its Contributions set forth herein, no assurances are + provided by any Contributor that the Program does not infringe the + patent or other intellectual property rights of any other entity. + Each Contributor disclaims any liability to Recipient for claims + brought by any other entity based on infringement of intellectual + property rights or otherwise. As a condition to exercising the + rights and licenses granted hereunder, each Recipient hereby + assumes sole responsibility to secure any other intellectual + property rights needed, if any. For example, if a third party + patent license is required to allow Recipient to Distribute the + Program, it is Recipient's responsibility to acquire that license + before distributing the Program. + + d) Each Contributor represents that to its knowledge it has + sufficient copyright rights in its Contribution, if any, to grant + the copyright license set forth in this Agreement. + + e) Notwithstanding the terms of any Secondary License, no + Contributor makes additional grants to any Recipient (other than + those set forth in this Agreement) as a result of such Recipient's + receipt of the Program under the terms of a Secondary License + (if permitted under the terms of Section 3). + +3. REQUIREMENTS + +3.1 If a Contributor Distributes the Program in any form, then: + + a) the Program must also be made available as Source Code, in + accordance with section 3.2, and the Contributor must accompany + the Program with a statement that the Source Code for the Program + is available under this Agreement, and informs Recipients how to + obtain it in a reasonable manner on or through a medium customarily + used for software exchange; and + + b) the Contributor may Distribute the Program under a license + different than this Agreement, provided that such license: + i) effectively disclaims on behalf of all other Contributors all + warranties and conditions, express and implied, including + warranties or conditions of title and non-infringement, and + implied warranties or conditions of merchantability and fitness + for a particular purpose; + + ii) effectively excludes on behalf of all other Contributors all + liability for damages, including direct, indirect, special, + incidental and consequential damages, such as lost profits; + + iii) does not attempt to limit or alter the recipients' rights + in the Source Code under section 3.2; and + + iv) requires any subsequent distribution of the Program by any + party to be under a license that satisfies the requirements + of this section 3. + +3.2 When the Program is Distributed as Source Code: + + a) it must be made available under this Agreement, or if the + Program (i) is combined with other material in a separate file or + files made available under a Secondary License, and (ii) the initial + Contributor attached to the Source Code the notice described in + Exhibit A of this Agreement, then the Program may be made available + under the terms of such Secondary Licenses, and + + b) a copy of this Agreement must be included with each copy of + the Program. + +3.3 Contributors may not remove or alter any copyright, patent, +trademark, attribution notices, disclaimers of warranty, or limitations +of liability ("notices") contained within the Program from any copy of +the Program which they Distribute, provided that Contributors may add +their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION + +Commercial distributors of software may accept certain responsibilities +with respect to end users, business partners and the like. While this +license is intended to facilitate the commercial use of the Program, +the Contributor who includes the Program in a commercial product +offering should do so in a manner which does not create potential +liability for other Contributors. Therefore, if a Contributor includes +the Program in a commercial product offering, such Contributor +("Commercial Contributor") hereby agrees to defend and indemnify every +other Contributor ("Indemnified Contributor") against any losses, +damages and costs (collectively "Losses") arising from claims, lawsuits +and other legal actions brought by a third party against the Indemnified +Contributor to the extent caused by the acts or omissions of such +Commercial Contributor in connection with its distribution of the Program +in a commercial product offering. The obligations in this section do not +apply to any claims or Losses relating to any actual or alleged +intellectual property infringement. In order to qualify, an Indemnified +Contributor must: a) promptly notify the Commercial Contributor in +writing of such claim, and b) allow the Commercial Contributor to control, +and cooperate with the Commercial Contributor in, the defense and any +related settlement negotiations. The Indemnified Contributor may +participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial +product offering, Product X. That Contributor is then a Commercial +Contributor. If that Commercial Contributor then makes performance +claims, or offers warranties related to Product X, those performance +claims and warranties are such Commercial Contributor's responsibility +alone. Under this section, the Commercial Contributor would have to +defend claims against the other Contributors related to those performance +claims and warranties, and if a court requires any other Contributor to +pay any damages as a result, the Commercial Contributor must pay +those damages. + +5. NO WARRANTY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" +BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR +IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF +TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR +PURPOSE. Each Recipient is solely responsible for determining the +appropriateness of using and distributing the Program and assumes all +risks associated with its exercise of rights under this Agreement, +including but not limited to the risks and costs of program errors, +compliance with applicable laws, damage to or loss of data, programs +or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY + +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT +PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS +SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST +PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE +EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL + +If any provision of this Agreement is invalid or unenforceable under +applicable law, it shall not affect the validity or enforceability of +the remainder of the terms of this Agreement, and without further +action by the parties hereto, such provision shall be reformed to the +minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity +(including a cross-claim or counterclaim in a lawsuit) alleging that the +Program itself (excluding combinations of the Program with other software +or hardware) infringes such Recipient's patent(s), then such Recipient's +rights granted under Section 2(b) shall terminate as of the date such +litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it +fails to comply with any of the material terms or conditions of this +Agreement and does not cure such failure in a reasonable period of +time after becoming aware of such noncompliance. If all Recipient's +rights under this Agreement terminate, Recipient agrees to cease use +and distribution of the Program as soon as reasonably practicable. +However, Recipient's obligations under this Agreement and any licenses +granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, +but in order to avoid inconsistency the Agreement is copyrighted and +may only be modified in the following manner. The Agreement Steward +reserves the right to publish new versions (including revisions) of +this Agreement from time to time. No one other than the Agreement +Steward has the right to modify this Agreement. The Eclipse Foundation +is the initial Agreement Steward. The Eclipse Foundation may assign the +responsibility to serve as the Agreement Steward to a suitable separate +entity. Each new version of the Agreement will be given a distinguishing +version number. The Program (including Contributions) may always be +Distributed subject to the version of the Agreement under which it was +received. In addition, after a new version of the Agreement is published, +Contributor may elect to Distribute the Program (including its +Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient +receives no rights or licenses to the intellectual property of any +Contributor under this Agreement, whether expressly, by implication, +estoppel or otherwise. All rights in the Program not expressly granted +under this Agreement are reserved. Nothing in this Agreement is intended +to be enforceable by any entity that is not a Contributor or Recipient. +No third-party beneficiary rights are created under this Agreement. + +Exhibit A - Form of Secondary Licenses Notice + +"This Source Code may also be made available under the following +Secondary Licenses when the conditions for such availability set forth +in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), +version(s), and exceptions or additional permissions here}." + + Simply including a copy of this Agreement, including this Exhibit A + is not sufficient to license the Source Code under Secondary Licenses. + + If it is not possible or desirable to put the notice in a particular + file, then You may include the notice in a location (such as a LICENSE + file in a relevant directory) where a recipient would be likely to + look for such a notice. + + You may add additional accurate notices of copyright ownership. diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..fe0932c --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,35 @@ +edition = "2021" +max_width = 140 +newline_style = "Unix" +unstable_features = true + +# always use tabs. +hard_tabs = true +tab_spaces = 4 + +# commas inbetween but not after +match_block_trailing_comma = true +trailing_comma = "Never" + +# fix my imports for me +imports_granularity = "Crate" +#group_imports = "One" + +# format everything +format_code_in_doc_comments = true +format_macro_matchers = true + +# don't keep outdated syntax +use_field_init_shorthand = true +use_try_shorthand = true + +# condense Struct { _, _ } to Struct { .. } +condense_wildcard_suffixes = true + +# prefer foo(Bar { \n }) over foo(\nBar { \n }\n) +overflow_delimited_expr = true + +# I wish there was a way to allow 0..n but not a + 1..b + 2 +# However, the later looks so terible that I use spaces everywhere +# https://github.com/rust-lang/rustfmt/issues/3367 +spaces_around_ranges = true diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..3b82759 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,23 @@ +mod setup; +mod steps; + +use crate::steps::{install_autostart, set_passwords}; +use setup::Setup; +use steps::{init_first_boot, init_network, init_os, install_packages, prepare_img}; + +const IMAGE_SIZE_GB: u64 = 2; + +fn main() -> anyhow::Result<()> { + let setup = Setup::load()?; + + let img = prepare_img()?; + init_os(&setup, &img)?; + init_network(&img, &setup)?; + install_packages(&img, &setup)?; + install_autostart(&img, &setup)?; + init_first_boot(&img)?; + set_passwords(&img, &setup)?; + + eprintln!("SUCCESS"); + Ok(()) +} diff --git a/src/steps/init_first_boot.rs b/src/steps/init_first_boot.rs new file mode 100644 index 0000000..1286143 --- /dev/null +++ b/src/steps/init_first_boot.rs @@ -0,0 +1,47 @@ +use super::{run_chroot, Image}; +use std::fs; + +pub fn init_first_boot(img: &Image) -> anyhow::Result<()> { + eprintln!("Preparing first boot script ..."); + + fs::write( + img.root.0.join("usr").join("local").join("bin").join("first-boot"), + r#"#!/bin/sh +set -xe + +cat < anyhow::Result<()> { + eprintln!("Preparing network ..."); + + let mut interfaces = File::create(img.root.0.join("etc").join("network").join("interfaces"))?; + for (name, ty) in [("lo", "loopback"), ("eth0", "dhcp")] { + writeln!(interfaces, "auto {name}")?; + writeln!(interfaces, "iface {name} inet {ty}")?; + writeln!(interfaces)?; + } + writeln!(interfaces, "hostname {}", setup.os.host.hostname)?; + fs::write( + img.root.0.join("etc").join("resolv.conf"), + r#"nameserver 1.0.0.1 +nameserver 1.1.1.1 +nameserver 2606:4700:4700::1001 +nameserver 2606:4700:4700::1111 + "# + )?; + run_chroot(img, "root", ["rc-update", "add", "networking", "default"])?; + + if setup.network.tailscale { + run_chroot(img, "root", "apk add tailscale; rc-update add tailscale default")?; + fs::write( + img.root.0.join("usr").join("local").join("bin").join("tailscale-up"), + r#"#!/bin/sh +for i in 0 1 2 3 4; do + ping -c 2 -W 1 1.1.1.1 && break + if [ "$i" == "4" ]; then exit 1; fi + echo "Waiting for network ..." + sleep 2s +done + +set -e + +echo "Connecting to tailscale ..." +timeout -v 2m tailscale up +"# + )?; + fs::write( + img.root.0.join("etc").join("init.d").join("tailscale-up"), + r#"#!/sbin/openrc-run +command="/usr/local/bin/tailscale-up" +command_background=false +depend() { + after ntpd + need tailscale +}"# + )?; + run_chroot( + img, + "root", + "chmod +x /usr/local/bin/tailscale-up /etc/init.d/tailscale-up; rc-update add tailscale-up default" + )?; + } + + if !setup.network.wifi.is_empty() { + run_chroot(img, "root", "echo 'brcmfmac' >> /etc/modules")?; + run_chroot(img, "root", ["apk", "add", "iw", "iwd", "wireless-tools", "wireless-regdb"])?; + run_chroot(img, "root", ["rc-update", "add", "iwd", "default"])?; + + let iwd = img.root.0.join("etc").join("iwd"); + fs::create_dir_all(&iwd).context("Unable to create /etc/iwd directory")?; + fs::write( + iwd.join("main.conf"), + r#"# main iwd configuration file + +[General] +EnableNetworkConfiguration=true + +[Network] +EnableIPv6=true +NameResolvingService=none +"# + ) + .context("Unable to write /etc/iwd/main.conf")?; + + let iwd = img.root.0.join("var").join("lib").join("iwd"); + fs::create_dir_all(&iwd).context("Unable to create /var/lib/iwd directory")?; + for wifi in &setup.network.wifi { + let ssid = &wifi.ssid; + match &wifi.security { + WifiSecurity::None => { + fs::write(iwd.join(format!("{ssid}.open")), "\n")?; + }, + WifiSecurity::WpaPsk { password } => { + fs::write(iwd.join(format!("{ssid}.psk")), format!("[Security]\nPassphrase={password}\n"))?; + }, + WifiSecurity::WpaEap { identity, password } => { + let mut file = File::create(iwd.join(format!("{ssid}.8021x")))?; + writeln!(file, "[Security]")?; + writeln!(file, "EAP-Method=PWD")?; + writeln!(file, "EAP-Identity={identity}")?; + writeln!(file, "EAP-Password={password}")?; + writeln!(file)?; + writeln!(file, "[Settings]")?; + writeln!(file, "AutoConnect=true")?; + } + } + } + } + + Ok(()) +} diff --git a/src/steps/init_os.rs b/src/steps/init_os.rs new file mode 100644 index 0000000..788e6a8 --- /dev/null +++ b/src/steps/init_os.rs @@ -0,0 +1,189 @@ +use super::{download, run, run_chroot, tempfile, Image}; +use crate::setup::Setup; +use anyhow::Context as _; +use std::{ + fs::{self, File}, + io::Write as _, + path::Path +}; + +pub fn init_os(setup: &Setup, img: &Image) -> anyhow::Result<()> { + eprintln!("Preparing operating system ..."); + + // write raspberry pi config + fs::write(img.boot.0.join("cmdline.txt"), &setup.os.rpi.cmdline)?; + let mut config = File::create(img.boot.0.join("config.txt"))?; + writeln!( + config, + r#"# Do not modify this file. Create and/or modify usercfg.txt instead. +[pi3] +kernel=vmlinuz-rpi +initramfs initramfs-rpi +[pi3+] +kernel=vmlinuz-rpi +initramfs initramfs-rpi +[pi4] +enable_gic=1 +kernel=vmlinuz-rpi4 +initramfs initramfs-rpi4 +[all]"# + )?; + if setup.os.alpine.arch.is64bit() { + writeln!(config, "arm_64bit=1")?; + } + writeln!(config, "include usercfg.txt")?; + drop(config); + fs::write(img.boot.0.join("usercfg.txt"), &setup.os.rpi.usercfg)?; + + // write initial apk config + let etc = img.root.0.join("etc"); + let etc_apk = etc.join("apk"); + let etc_apk_keys = etc_apk.join("keys"); + fs::create_dir_all(&etc_apk_keys).context("Failed to create /etc/apk/keys directory")?; + let mut repositories = File::create(etc_apk.join("repositories"))?; + for repo in ["main", "community"] { + writeln!(repositories, "{}/{}/{repo}", setup.os.alpine.mirror, setup.os.alpine.branch)?; + } + for repo in &setup.os.alpine.extra_repos { + writeln!(repositories, "{repo}")?; + } + drop(repositories); + + // download apk keys + let keys = tempfile()?; + download( + keys.path(), + &format!( + "https://gitlab.alpinelinux.org/alpine/aports/-/archive/{branch}/aports-{branch}.tar.bz2?path=main/alpine-keys", + branch = setup.os.alpine.branch.git_branch() + ) + )?; + run(&[ + "tar", + "xfj", + keys.path().to_str().unwrap(), + "-C", + etc_apk_keys.to_str().unwrap(), + "--strip-components=3", + "--wildcards", + "*/alpine-devel@lists.alpinelinux.org-*.rsa.pub" + ])?; + drop(keys); + + // copy additional keys + for key in &setup.os.alpine.extra_keys { + let key = Path::new(key); + let name = key.file_name().unwrap(); + fs::copy(Path::new("setup").join(key), etc_apk_keys.join(name))?; + } + + // bootstrap alpine + run(&[ + "apk", + "add", + "--root", + &img.root, + "--update-cache", + "--initdb", + "--arch", + setup.os.alpine.arch.to_str(), + "agetty", + "alpine-base", + "alpine-sdk", + "ca-certificates", + "elogind", + "eudev", + "haveged", + "linux-rpi", + "linux-rpi4", + "openssh", + "parted", + "raspberrypi-bootloader", + "sudo", + "tzdata", + "udev-init-scripts", + "util-linux-login", + "zram-init" + ])?; + run(&[ + "sed", + "-E", + "-e", + "s,getty(.*)$,agetty --noclear\\1 linux,", + "-i", + etc.join("inittab").to_str().unwrap() + ])?; + for service in ["udev", "udev-postmount", "udev-settle", "udev-trigger"] { + run_chroot(img, "root", ["rc-update", "add", service, "sysinit"])?; + } + for service in ["modules", "syslog", "swclock"] { + run_chroot(img, "root", ["rc-update", "add", service, "boot"])?; + } + for service in ["elogind", "haveged", "ntpd", "sshd"] { + run_chroot(img, "root", ["rc-update", "add", service, "default"])?; + } + + // configure fstab + fs::write( + etc.join("fstab"), + r#"# +/dev/mmcblk0p1 /boot vfat defaults 0 2 +/dev/mmcblk0p2 / ext4 defaults,noatime 0 1 +"# + )?; + + // something is broken with abuild's functions.sh, so let's set CBUILD ourselves + let profile_d = etc.join("profile.d"); + fs::create_dir_all(&profile_d)?; + fs::write( + profile_d.join("cbuild.sh"), + &format!("export CBUILD={}", setup.os.alpine.arch.cbuild()) + )?; + + // set up the locale + run_chroot(img, "root", ["setup-hostname", &setup.os.host.hostname])?; + run_chroot(img, "root", ["setup-timezone", "-z", &setup.os.host.timezone])?; + if let Some(variant) = setup.os.host.keymap_variant.as_deref() { + run_chroot(img, "root", ["setup-keymap", &setup.os.host.keymap, variant])?; + } else { + run_chroot(img, "root", ["setup-keymap", &setup.os.host.keymap])?; + } + + // set up a user that we can use to install packages + run_chroot(img, "root", ["adduser", "-D", "mkalpiimg"])?; + run_chroot(img, "root", ["adduser", "mkalpiimg", "abuild"])?; + run_chroot(img, "root", "echo mkalpiimg:mkalpiimg | chpasswd")?; + fs::write(etc.join("sudoers.d").join("mkalpiimg"), "%abuild ALL=(ALL) NOPASSWD: ALL")?; + fs::create_dir_all(img.root.0.join("var").join("cache").join("distfiles"))?; + run_chroot(img, "root", "chgrp abuild /var/cache/distfiles; chmod 775 /var/cache/distfiles")?; + run_chroot(img, "mkalpiimg", ["abuild-keygen", "-a", "-n", "-b", "4096"])?; + run_chroot(img, "root", "cp /home/mkalpiimg/.abuild/*.rsa.pub /etc/apk/keys/")?; + + // avoid a bug with cargo in qemu: https://github.com/rust-lang/cargo/issues/10583 + let cargo = img.root.0.join("home").join("mkalpiimg").join(".cargo"); + fs::create_dir_all(&cargo)?; + fs::write(cargo.join("config.toml"), "[net]\ngit-fetch-with-cli = true\n")?; + + // set up the other users as requested + fs::write(etc.join("sudoers.d").join("wheel"), "%wheel ALL=(ALL:ALL) ALL")?; + for user in &setup.os.user { + run_chroot(img, "root", ["adduser", "-s", "/bin/ash", "-D", &user.name])?; + if user.sudo { + run_chroot(img, "root", ["adduser", &user.name, "wheel"])?; + } + for grp in &user.groups { + run_chroot(img, "root", ["adduser", &user.name, grp])?; + } + let ssh = img.root.0.join("home").join(&user.name).join(".ssh"); + fs::create_dir_all(&ssh)?; + let mut keys = File::create(ssh.join("authorized_keys"))?; + for key in &user.authorized_keys { + writeln!(keys, "{key}")?; + } + drop(keys); + run_chroot(img, "root", ["chown", "-R", &user.name, &format!("/home/{}/.ssh", user.name)])?; + run_chroot(img, &user.name, "chmod 700 ~/.ssh; chmod 600 ~/.ssh/*")?; + } + + Ok(()) +} diff --git a/src/steps/install_packages.rs b/src/steps/install_packages.rs new file mode 100644 index 0000000..a6206aa --- /dev/null +++ b/src/steps/install_packages.rs @@ -0,0 +1,52 @@ +use super::{run_chroot, Image}; +use crate::setup::Setup; + +pub fn install_packages(img: &Image, setup: &Setup) -> anyhow::Result<()> { + eprintln!("Installing packages ..."); + + for pkg in &setup.packages.install { + run_chroot(img, "root", ["apk", "add", pkg])?; + } + + for service in &setup.packages.autostart { + run_chroot(img, "root", ["rc-update", "add", service, "default"])?; + } + + Ok(()) +} + +pub fn install_autostart(img: &Image, setup: &Setup) -> anyhow::Result<()> { + // TODO make the kiosk optional maybe? + let kiosk = &setup.autostart.kiosk; + eprintln!("Installing kiosk ..."); + run_chroot(img, "root", [ + "apk", + "add", + "cage", + "firefox-esr", + "font-noto", + "mesa-dri-gallium", + "polkit", + "polkit-elogind" + ])?; + run_chroot(img, "root", [ + "sed", + "-E", + "-i", + "-e", + &format!("/tty1/s,agetty,agetty --autologin {},", kiosk.user), + "/etc/inittab" + ])?; + run_chroot(img, &kiosk.user, "[ -e ~/.profile ] || echo '#!/bin/busybox ash' >~/.profile")?; + run_chroot( + img, + &kiosk.user, + format!( + r#"echo 'if [ "$(tty)" == "/dev/tty1" ]; then dbus-run-session cage firefox-esr --kiosk "{}"; fi' >>~/.profile"#, + kiosk.page + ) + .as_str() + )?; + + Ok(()) +} diff --git a/src/steps/mod.rs b/src/steps/mod.rs new file mode 100644 index 0000000..83ac63d --- /dev/null +++ b/src/steps/mod.rs @@ -0,0 +1,183 @@ +use anyhow::{anyhow, bail, Context as _}; +use std::{ + fmt::Write as _, + fs::File, + io::{self, BufWriter}, + ops::Deref, + os::unix::process::ExitStatusExt as _, + path::{Path, PathBuf}, + process::Command +}; +use tempfile::{NamedTempFile, TempDir}; + +fn tempfile() -> io::Result { + NamedTempFile::new() +} + +fn run(cmd: &[&str]) -> anyhow::Result<()> { + let display = format!("`\"{}\"`", cmd.join("\" \"")); + eprintln!(" -> Running {display} ..."); + + let status = match Command::new(cmd[0]).args(cmd.iter().skip(1)).status() { + Ok(status) => status, + Err(err) => bail!("Failed to run `\"{}\"`: {err}", cmd.join("\" \"")) + }; + if !status.success() { + bail!( + "Failed to run `\"{}\"`: Process returned exit code {}", + cmd.join("\" \""), + status.into_raw() + ); + } + Ok(()) +} + +fn run_output(cmd: &[&str]) -> anyhow::Result { + let display = format!("`\"{}\"`", cmd.join("\" \"")); + eprintln!(" -> Running {display} ..."); + + let output = match Command::new(cmd[0]).args(cmd.iter().skip(1)).output() { + Ok(output) => output, + Err(err) => bail!("Failed to run {display}: {err}") + }; + if !output.status.success() { + bail!("Failed to run {display}: Process returned exit code {}", output.status.into_raw()); + } + String::from_utf8(output.stdout).context("The output of {display} contained invalid characters") +} + +macro_rules! run_parted { + ($path:expr, $($arg:expr),+) => { + run(&["parted", "-s", $path.to_str().unwrap(), $($arg,)+]) + }; +} + +trait ChrootCmd { + fn into_shell_cmd(self) -> String; +} + +impl ChrootCmd for [&str; N] { + fn into_shell_cmd(self) -> String { + (&self[..]).into_shell_cmd() + } +} + +impl ChrootCmd for &[&str] { + fn into_shell_cmd(self) -> String { + let mut cmd = String::new(); + for arg in self { + write!(cmd, "'{}' ", arg.replace('\'', "'\"'\"'")).unwrap(); + } + cmd + } +} + +impl ChrootCmd for &str { + fn into_shell_cmd(self) -> String { + self.into() + } +} + +fn run_chroot(img: &Image, user: &str, cmd: impl ChrootCmd) -> anyhow::Result<()> { + let cmd = format!(". /etc/profile; set -euo pipefail; {}", cmd.into_shell_cmd()); + run(&[ + "chroot", + &img.root, + "/usr/bin/env", + "-i", + "sudo", + "-u", + user, + "-i", + "-n", + "ash", + "-c", + &cmd + ]) +} + +fn download(out: &Path, url: &str) -> anyhow::Result<()> { + eprintln!(" -> Downloading {url} ..."); + attohttpc::get(url).send()?.write_to(BufWriter::new(File::create(out)?))?; + Ok(()) +} + +pub struct Image { + path: PathBuf, + boot: Mount, + dev: Mount, + root: Mount, + device: LoopDevice +} + +impl Drop for Image { + fn drop(&mut self) { + eprintln!("Cleaning up ..."); + } +} + +struct LoopDevice(String); + +impl LoopDevice { + fn part(&self, num: usize) -> String { + format!("{}p{num}", self.0) + } +} + +impl Deref for LoopDevice { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Drop for LoopDevice { + fn drop(&mut self) { + let res = run(&["udisksctl", "loop-delete", "--no-user-interaction", "-b", self]); + if let Err(err) = res { + eprintln!("Failed to free loop device {}: {err}", self.0); + } + } +} + +struct Mount(PathBuf); + +impl Mount { + fn create(device: &str) -> anyhow::Result { + let path = TempDir::new()?.into_path(); + run(&["mount", device, &path.to_string_lossy()])?; + Ok(Self(path)) + } +} + +impl Deref for Mount { + type Target = str; + + fn deref(&self) -> &Self::Target { + self.0.to_str().unwrap() + } +} + +impl Drop for Mount { + fn drop(&mut self) { + let res = run(&["umount", self]); + if let Err(err) = res { + eprintln!("Failed to unmount {}: {err}", self.0.display()); + } + } +} + +mod init_first_boot; +mod init_network; +mod init_os; +mod install_packages; +mod prepare_img; +mod set_passwords; + +pub use init_first_boot::init_first_boot; +pub use init_network::init_network; +pub use init_os::init_os; +pub use install_packages::{install_autostart, install_packages}; +pub use prepare_img::prepare_img; +pub use set_passwords::set_passwords; diff --git a/src/steps/prepare_img.rs b/src/steps/prepare_img.rs new file mode 100644 index 0000000..ef569db --- /dev/null +++ b/src/steps/prepare_img.rs @@ -0,0 +1,57 @@ +use super::{run, run_output, Image, LoopDevice, Mount}; +use crate::IMAGE_SIZE_GB; +use anyhow::{anyhow, Context as _}; +use std::{ + fs::{self, File}, + path::PathBuf +}; + +pub fn prepare_img() -> anyhow::Result { + eprintln!("Preparing empty image ..."); + + // let's create a file with the correct size + let path = PathBuf::from("alpi.img"); + let file = File::create(&path).context("Failed to create alpi.img")?; + file.set_len(IMAGE_SIZE_GB * 1024 * 1024 * 1024)?; + drop(file); + + // lets get the thing partitioned + let size = format!("{IMAGE_SIZE_GB}G"); + run_parted!(path, "mktable", "msdos")?; + run_parted!(path, "mkpart", "primary", "fat32", "0G", "128M")?; + run_parted!(path, "mkpart", "primary", "ext4", "128M", &size)?; + + // create a loopback device + let output = run_output(&["udisksctl", "loop-setup", "--no-user-interaction", "-f", path.to_str().unwrap()])?; + print!("{output}"); + let idx = output.find("/dev/loop").ok_or_else(|| anyhow!("Unable to identify loop device"))?; + let mut end = idx + "/dev/loop".len(); + while (output.as_bytes()[end] as char).is_ascii_digit() { + end += 1; + } + let device = LoopDevice(output[idx .. end].to_owned()); + + // TODO: Somehow create the file systems without root + run(&["mkfs.fat", "-n", "ALPI-BOOT", "-F", "32", &device.part(1)])?; + run(&["mkfs.ext4", "-q", "-F", "-L", "ALPI", "-O", "^has_journal", &device.part(2)])?; + run_parted!(path, "p")?; + + // mount everything + let root = Mount::create(&device.part(2))?; + let boot = root.0.join("boot"); + fs::create_dir_all(&boot)?; + run(&["mount", &device.part(1), boot.to_str().unwrap()])?; + let boot = Mount(boot); + let dev = root.0.join("dev"); + fs::create_dir_all(&dev)?; + run(&["mount", "udev", dev.to_str().unwrap(), "-t", "devtmpfs", "-o", "mode=0755,nosuid"])?; + let dev = Mount(dev); + + Ok(Image { + path, + device, + boot, + dev, + root + }) +} diff --git a/src/steps/set_passwords.rs b/src/steps/set_passwords.rs new file mode 100644 index 0000000..f401be6 --- /dev/null +++ b/src/steps/set_passwords.rs @@ -0,0 +1,21 @@ +use super::{run_chroot, Image}; +use crate::setup::Setup; +use anyhow::bail; + +pub fn set_passwords(img: &Image, setup: &Setup) -> anyhow::Result<()> { + eprintln!("Setting user passwords ..."); + + // the password is insecure by design, so we'll lock it for security + run_chroot(img, "root", ["passwd", "-l", "mkalpiimg"])?; + + for user in &setup.os.user { + if let Some(password) = &user.password { + if password.contains('\'') { + bail!("Sorry I won't set passwords containing `'`"); + } + run_chroot(img, "root", format!("echo '{}:{password}' | chpasswd", user.name).as_str())?; + } + } + + Ok(()) +}