diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d0693d6 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,48 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.h] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.rs] +indent_style = space +indent_size = 4 + +[*.sh] +indent_style = tab +indent_size = 4 + +[*.toml] +indent_style = space +indent_size = 4 + +[*.{yaml,yml}] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = false + +[.github/{actions,workflows}/**/*.yml] +indent_style = space +indent_size = 2 + +[Cargo.toml] +indent_style = space +indent_size = 4 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..6377580 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,10 @@ +* text=auto eol=lf + +*.h text eol=lf +*.md text eol=lf +*.rs text eol=lf +*.sh text eol=lf +*.toml text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +Cargo.lock text eol=lf merge=binary diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..66feae7 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,491 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "0.6.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b" + +[[package]] +name = "anstyle-parse" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19" +dependencies = [ + "anstyle", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.83" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" + +[[package]] +name = "bindgen" +version = "0.69.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "itertools", + "lazy_static", + "lazycell", + "log", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + +[[package]] +name = "bpaf" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3280efcf6d66bc77c2cf9b67dc8acee47a217d9be67dd590b3230dffe663724d" + +[[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 = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "clang-sys" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67523a3b4be3ce1989d607a828d036249522dd9c1c8de7f4dd2dae43a37369d1" +dependencies = [ + "glob", + "libc", +] + +[[package]] +name = "colorchoice" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" + +[[package]] +name = "either" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2" + +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", +] + +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "humantime", + "log", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "fastrand" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" + +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800" + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "krun" +version = "0.1.0" +dependencies = [ + "anyhow", + "bpaf", + "env_logger", + "krun-sys", + "log", + "nix", + "rustix", + "utils", +] + +[[package]] +name = "krun-guest" +version = "0.1.0" +dependencies = [ + "anyhow", + "bpaf", + "env_logger", + "log", + "nix", + "rustix", + "tempfile", + "utils", +] + +[[package]] +name = "krun-sys" +version = "1.8.1" +dependencies = [ + "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.154" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" + +[[package]] +name = "memchr" +version = "2.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nix" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[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 = "proc-macro2" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustix" +version = "0.38.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "syn" +version = "2.0.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tempfile" +version = "3.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +dependencies = [ + "cfg-if", + "fastrand", + "rustix", + "windows-sys", +] + +[[package]] +name = "unicode-ident" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" + +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + +[[package]] +name = "utils" +version = "0.0.0" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ada9b01 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,24 @@ +[workspace] +resolver = "2" +members = ["crates/*"] + +[workspace.package] +version = "0.1.0" +authors = ["Sergio Lopez ", "Teoh Han Hui "] +edition = "2021" +rust-version = "1.77.0" +description = "Run programs from your system in a microVM" +repository = "https://github.com/slp/krun" +license = "MIT" + +[workspace.dependencies] +anyhow = { version = "1.0.82", default-features = false } +bindgen = { version = "0.69.4", default-features = false } +bpaf = { version = "0.9.11", default-features = false } +env_logger = { version = "0.11.3", default-features = false } +krun-sys = { path = "crates/krun-sys", default-features = false } +log = { version = "0.4.21", default-features = false } +nix = { version = "0.28.0", default-features = false } +rustix = { version = "0.38.34", default-features = false } +tempfile = { version = "3.10.1", default-features = false } +utils = { path = "crates/utils", default-features = false } diff --git a/crates/krun-guest/Cargo.toml b/crates/krun-guest/Cargo.toml new file mode 100644 index 0000000..03e3c50 --- /dev/null +++ b/crates/krun-guest/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "krun-guest" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +description = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +anyhow = { workspace = true, features = ["std"] } +bpaf = { workspace = true, features = [] } +env_logger = { workspace = true, features = ["auto-color", "humantime", "unstable-kv"] } +log = { workspace = true, features = ["kv"] } +nix = { workspace = true, features = ["user"] } +rustix = { workspace = true, features = ["fs", "mount", "std", "system", "use-libc-auxv"] } +tempfile = { workspace = true, features = [] } +utils = { workspace = true, features = [] } + +[features] +default = [] diff --git a/crates/krun-guest/src/bin/krun-guest.rs b/crates/krun-guest/src/bin/krun-guest.rs new file mode 100644 index 0000000..4463b41 --- /dev/null +++ b/crates/krun-guest/src/bin/krun-guest.rs @@ -0,0 +1,37 @@ +use std::{os::unix::process::CommandExt as _, process::Command}; + +use anyhow::{Context, Result}; +use krun_guest::{ + cli_options::options, fex::setup_fex, mount::mount_filesystems, net::configure_network, + sommelier::exec_sommelier, user::setup_user, +}; +use log::debug; + +fn main() -> Result<()> { + env_logger::init(); + + let options = options().run(); + + if let Err(err) = mount_filesystems() { + return Err(err).context("Couldn't mount filesystems, bailing out"); + } + + setup_fex()?; + + configure_network()?; + + if let Err(err) = setup_user(options.username, options.uid, options.gid) { + return Err(err).context("Couldn't set up user, bailing out"); + } + + // Will not return if successful. + exec_sommelier(&options.command, &options.command_args) + .context("Failed to execute sommelier")?; + + // Fallback option if sommelier is not present. + debug!(command:% = options.command, command_args:? = options.command_args; "exec"); + let err = Command::new(&options.command) + .args(options.command_args) + .exec(); + Err(err).with_context(|| format!("Failed to exec `{}`", options.command))? +} diff --git a/crates/krun-guest/src/cli_options.rs b/crates/krun-guest/src/cli_options.rs new file mode 100644 index 0000000..122fbb4 --- /dev/null +++ b/crates/krun-guest/src/cli_options.rs @@ -0,0 +1,54 @@ +use anyhow::Context; +use bpaf::{any, construct, positional, OptionParser, Parser}; +use nix::{ + libc::{gid_t, uid_t}, + unistd::{Gid, Uid}, +}; + +#[derive(Clone, Debug)] +pub struct Options { + pub username: String, + pub uid: Uid, + pub gid: Gid, + pub command: String, + pub command_args: Vec, +} + +pub fn options() -> OptionParser { + let username = positional("USER"); + let uid = positional::("UID").parse(|s| { + s.parse::() + .context("Failed to parse UID") + .map(|uid| uid.into()) + }); + let gid = positional::("GID").parse(|s| { + s.parse::() + .context("Failed to parse GID") + .map(|gid| gid.into()) + }); + let command = positional("COMMAND"); + let command_args = any::("COMMAND_ARGS", |arg| { + (!["--help", "-h"].contains(&&*arg)).then_some(arg) + }) + .many(); + + construct!(Options { + // positionals + username, + uid, + gid, + command, + command_args, + }) + .to_options() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_options() { + options().check_invariants(false) + } +} diff --git a/crates/krun-guest/src/fex.rs b/crates/krun-guest/src/fex.rs new file mode 100644 index 0000000..abd919a --- /dev/null +++ b/crates/krun-guest/src/fex.rs @@ -0,0 +1,36 @@ +use std::{fs::OpenOptions, io::Write}; + +use anyhow::{Context, Result}; +use utils::env::find_in_path; + +const FEX_X86_BINFMT_MISC_RULE: &str = ":FEX-x86:M:0:\\x7fELF\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x03\\x00:\\xff\\xff\\xff\\xff\\xff\\xfe\\xfe\\x00\\x00\\x00\\x00\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff:${FEX_INTERPRETER}:POCF"; +const FEX_X86_64_BINFMT_MISC_RULE: &str = ":FEX-x86_64:M:0:\\x7fELF\\x02\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x00\\x3e\\x00:\\xff\\xff\\xff\\xff\\xff\\xfe\\xfe\\x00\\x00\\x00\\x00\\xff\\xff\\xff\\xff\\xff\\xfe\\xff\\xff\\xff:${FEX_INTERPRETER}:POCF"; + +pub fn setup_fex() -> Result<()> { + let fex_interpreter_path = + find_in_path("FEXInterpreter").context("Failed to check existence of `FEXInterpreter`")?; + let Some(fex_interpreter_path) = fex_interpreter_path else { + return Ok(()); + }; + let fex_interpreter_path = fex_interpreter_path + .to_str() + .context("Failed to process `FEXInterpreter` path as it contains invalid UTF-8")?; + + let mut file = OpenOptions::new() + .write(true) + .open("/proc/sys/fs/binfmt_misc/register") + .context("Failed to open binfmt_misc/register for writing")?; + + { + let rule = FEX_X86_BINFMT_MISC_RULE.replace("${FEX_INTERPRETER}", fex_interpreter_path); + file.write_all(rule.as_bytes()) + .context("Failed to register `FEX-x86` binfmt_misc rule")?; + } + { + let rule = FEX_X86_64_BINFMT_MISC_RULE.replace("${FEX_INTERPRETER}", fex_interpreter_path); + file.write_all(rule.as_bytes()) + .context("Failed to register `FEX-x86_64` binfmt_misc rule")?; + } + + Ok(()) +} diff --git a/crates/krun-guest/src/lib.rs b/crates/krun-guest/src/lib.rs new file mode 100644 index 0000000..4452084 --- /dev/null +++ b/crates/krun-guest/src/lib.rs @@ -0,0 +1,6 @@ +pub mod cli_options; +pub mod fex; +pub mod mount; +pub mod net; +pub mod sommelier; +pub mod user; diff --git a/crates/krun-guest/src/mount.rs b/crates/krun-guest/src/mount.rs new file mode 100644 index 0000000..13d1e9b --- /dev/null +++ b/crates/krun-guest/src/mount.rs @@ -0,0 +1,54 @@ +use std::{fs::OpenOptions, os::fd::AsFd}; + +use anyhow::{Context, Result}; +use rustix::{ + fs::CWD, + mount::{mount2, move_mount, open_tree, MountFlags, MoveMountFlags, OpenTreeFlags}, +}; + +pub fn mount_filesystems() -> Result<()> { + mount2( + Some("tmpfs"), + "/var/run", + Some("tmpfs"), + MountFlags::NOEXEC | MountFlags::NOSUID | MountFlags::RELATIME, + None, + ) + .context("Failed to mount `/var/run`")?; + + let _ = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open("/tmp/resolv.conf") + .context("Failed to create `/tmp/resolv.conf`")?; + + { + let fd = open_tree( + CWD, + "/tmp/resolv.conf", + OpenTreeFlags::OPEN_TREE_CLONE | OpenTreeFlags::OPEN_TREE_CLOEXEC, + ) + .context("Failed to open_tree `/tmp/resolv.conf`")?; + + move_mount( + fd.as_fd(), + "", + CWD, + "/etc/resolv.conf", + MoveMountFlags::MOVE_MOUNT_F_EMPTY_PATH, + ) + .context("Failed to move_mount `/etc/resolv.conf`")?; + } + + mount2( + Some("binfmt_misc"), + "/proc/sys/fs/binfmt_misc", + Some("binfmt_misc"), + MountFlags::NOEXEC | MountFlags::NOSUID | MountFlags::RELATIME, + None, + ) + .context("Failed to mount `binfmt_misc`")?; + + Ok(()) +} diff --git a/crates/krun-guest/src/net.rs b/crates/krun-guest/src/net.rs new file mode 100644 index 0000000..3710bd5 --- /dev/null +++ b/crates/krun-guest/src/net.rs @@ -0,0 +1,39 @@ +use std::{fs, os::unix::process::ExitStatusExt, process::Command}; + +use anyhow::{anyhow, Context, Result}; +use log::debug; +use rustix::system::sethostname; + +pub fn configure_network() -> Result<()> { + { + let hostname = + fs::read_to_string("/etc/hostname").context("Failed to read `/etc/hostname`")?; + let hostname = if let Some((hostname, _)) = hostname.split_once('\n') { + hostname.to_owned() + } else { + hostname + }; + sethostname(hostname.as_bytes()).context("Failed to set hostname")?; + } + + let output = Command::new("/sbin/dhclient") + .output() + .context("Failed to execute `dhclient` as child process")?; + debug!(output:?; "dhclient output"); + if !output.status.success() { + let err = if let Some(code) = output.status.code() { + anyhow!("`dhclient` process exited with status code: {code}") + } else { + anyhow!( + "`dhclient` process terminated by signal: {}", + output + .status + .signal() + .expect("either one of status code or signal should be set") + ) + }; + Err(err)?; + } + + Ok(()) +} diff --git a/crates/krun-guest/src/sommelier.rs b/crates/krun-guest/src/sommelier.rs new file mode 100644 index 0000000..6b80249 --- /dev/null +++ b/crates/krun-guest/src/sommelier.rs @@ -0,0 +1,28 @@ +use std::{env, os::unix::process::CommandExt as _, process::Command}; + +use anyhow::{Context, Result}; +use log::debug; +use utils::env::find_in_path; + +pub fn exec_sommelier(command: &String, command_args: &[String]) -> Result<()> { + let sommelier_path = + find_in_path("sommelier").context("Failed to check existence of `sommelier`")?; + let Some(sommelier_path) = sommelier_path else { + return Ok(()); + }; + + let gl_env = env::var("LIBGL_DRIVERS_PATH").ok(); + + let mut cmd = Command::new(sommelier_path); + cmd.args(["--virtgpu-channel", "-X", "--glamor"]); + + if let Some(gl_env) = gl_env { + cmd.arg(format!("--xwayland-gl-driver-path={}", gl_env)); + } + + cmd.arg(command).args(command_args); + + debug!(command:%, command_args:?; "exec"); + let err = cmd.exec(); + Err(err).context("Failed to exec `sommelier`")? +} diff --git a/crates/krun-guest/src/user.rs b/crates/krun-guest/src/user.rs new file mode 100644 index 0000000..6d6e72d --- /dev/null +++ b/crates/krun-guest/src/user.rs @@ -0,0 +1,57 @@ +use std::{ + env, + fs::{read_dir, Permissions}, + os::unix::fs::{chown, PermissionsExt as _}, +}; + +use anyhow::{anyhow, Context, Result}; +use nix::unistd::{setgid, setuid, Gid, Uid, User}; + +pub fn setup_user(username: String, uid: Uid, gid: Gid) -> Result<()> { + setup_directories(uid, gid)?; + + setgid(gid).context("Failed to setgid")?; + setuid(uid).context("Failed to setuid")?; + + { + let path = tempfile::Builder::new() + .prefix(&format!("krun-run-{uid}-")) + .permissions(Permissions::from_mode(0o755)) + .tempdir() + .context("Failed to create temp dir for `XDG_RUNTIME_DIR`")? + .into_path(); + // SAFETY: Safe if and only if `krun-guest` program is not multithreaded. + // See https://doc.rust-lang.org/std/env/fn.set_var.html#safety + env::set_var("XDG_RUNTIME_DIR", path); + } + + let user = User::from_name(&username) + .map_err(Into::into) + .and_then(|user| user.ok_or_else(|| anyhow!("requested entry not found"))) + .with_context(|| format!("Failed to get user `{username}` from user database"))?; + + { + // SAFETY: Safe if and only if `krun-guest` program is not multithreaded. + // See https://doc.rust-lang.org/std/env/fn.set_var.html#safety + env::set_var("HOME", user.dir); + } + + Ok(()) +} + +fn setup_directories(uid: Uid, gid: Gid) -> Result<()> { + for dir in ["/dev/dri", "/dev/snd"] { + let dir_iter = + read_dir(dir).with_context(|| format!("Failed to read directory `{dir}`"))?; + + for entry in dir_iter { + let path = entry + .with_context(|| format!("Failed to read directory entry in `{dir}`"))? + .path(); + chown(&path, Some(uid.into()), Some(gid.into())) + .with_context(|| format!("Failed to chown {path:?}"))?; + } + } + + Ok(()) +} diff --git a/crates/krun-sys/Cargo.toml b/crates/krun-sys/Cargo.toml new file mode 100644 index 0000000..8b7082b --- /dev/null +++ b/crates/krun-sys/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "krun-sys" +version = "1.8.1" +edition = { workspace = true } +rust-version = { workspace = true } +repository = "https://github.com/containers/libkrun" +license = "Apache-2.0" +links = "krun" +publish = false + +[build-dependencies] +bindgen = { workspace = true, features = ["logging"] } + +[features] +default = [] diff --git a/crates/krun-sys/build.rs b/crates/krun-sys/build.rs new file mode 100644 index 0000000..86828b7 --- /dev/null +++ b/crates/krun-sys/build.rs @@ -0,0 +1,17 @@ +use std::{env, path::PathBuf}; + +fn main() { + println!("cargo::rerun-if-changed=wrapper.h"); + println!("cargo::rustc-link-lib=krun"); + + let bindings = bindgen::Builder::default() + .header("wrapper.h") + .clang_arg("-fretain-comments-from-system-headers") + .generate() + .expect("Unable to generate bindings"); + + let out_path = PathBuf::from(env::var("OUT_DIR").unwrap()); + bindings + .write_to_file(out_path.join("bindings.rs")) + .expect("Couldn't write bindings!"); +} diff --git a/crates/krun-sys/src/lib.rs b/crates/krun-sys/src/lib.rs new file mode 100644 index 0000000..32aa412 --- /dev/null +++ b/crates/krun-sys/src/lib.rs @@ -0,0 +1,5 @@ +#![allow(non_camel_case_types)] +#![allow(non_snake_case)] +#![allow(non_upper_case_globals)] + +include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/crates/krun-sys/wrapper.h b/crates/krun-sys/wrapper.h new file mode 100644 index 0000000..fba0181 --- /dev/null +++ b/crates/krun-sys/wrapper.h @@ -0,0 +1 @@ +#include diff --git a/crates/krun/Cargo.toml b/crates/krun/Cargo.toml new file mode 100644 index 0000000..755f685 --- /dev/null +++ b/crates/krun/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "krun" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +description = { workspace = true } +repository = { workspace = true } +license = { workspace = true } + +[dependencies] +anyhow = { workspace = true, features = ["std"] } +bpaf = { workspace = true, features = [] } +env_logger = { workspace = true, features = ["auto-color", "humantime", "unstable-kv"] } +krun-sys = { workspace = true, features = [] } +log = { workspace = true, features = ["kv"] } +nix = { workspace = true, features = ["user"] } +rustix = { workspace = true, features = ["net", "process", "std", "use-libc-auxv"] } +utils = { workspace = true, features = [] } + +[features] +default = [] diff --git a/crates/krun/src/bin/krun.rs b/crates/krun/src/bin/krun.rs new file mode 100644 index 0000000..c3a5258 --- /dev/null +++ b/crates/krun/src/bin/krun.rs @@ -0,0 +1,284 @@ +use std::{ + env::{self, VarError}, + ffi::{c_char, CString}, + os::fd::{IntoRawFd, OwnedFd}, +}; + +use anyhow::{anyhow, Context, Result}; +use krun::{ + cli_options::options, + net::{connect_to_passt, start_passt, NetMode}, +}; +use krun_sys::{ + krun_create_ctx, krun_set_exec, krun_set_gpu_options, krun_set_log_level, krun_set_passt_fd, + krun_set_root, krun_set_vm_config, krun_set_workdir, krun_start_enter, VIRGLRENDERER_DRM, + VIRGLRENDERER_THREAD_SYNC, VIRGLRENDERER_USE_ASYNC_FENCE_CB, VIRGLRENDERER_USE_EGL, +}; +use log::debug; +use nix::unistd::User; +use rustix::{ + io::Errno, + process::{geteuid, getgid, getrlimit, getuid, setrlimit, Resource}, +}; +use utils::env::find_in_path; + +fn main() -> Result<()> { + env_logger::init(); + + if getuid().as_raw() == 0 || geteuid().as_raw() == 0 { + println!("Running as root is not supported as it may break your system"); + return Err(anyhow!("real user ID or effective user ID is 0")); + } + + let options = options().fallback_to_usage().run(); + + { + // Set the log level to "off". + // + // SAFETY: Safe as no pointers involved. + let err = unsafe { krun_set_log_level(0) }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err).context("Failed to configure log level"); + } + } + + let ctx_id = { + // Create the configuration context. + // + // SAFETY: Safe as no pointers involved. + let ctx_id = unsafe { krun_create_ctx() }; + if ctx_id < 0 { + let err = Errno::from_raw_os_error(-ctx_id); + return Err(err).context("Failed to create configuration context"); + } + ctx_id as u32 + }; + + { + // Configure the number of vCPUs (4) and the amount of RAM (4096 MiB). + // + // SAFETY: Safe as no pointers involved. + let err = unsafe { krun_set_vm_config(ctx_id, 4, 4096) }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err) + .context("Failed to configure the number of vCPUs and/or the amount of RAM"); + } + } + + { + // Raise RLIMIT_NOFILE to the maximum allowed to create some room for virtio-fs + let mut rlim = getrlimit(Resource::Nofile); + rlim.current = rlim.maximum; + setrlimit(Resource::Nofile, rlim).context("Failed to raise `RLIMIT_NOFILE`")?; + } + + { + // SAFETY: `root_path` is a pointer to a C-string literal. + let err = unsafe { krun_set_root(ctx_id, c"/".as_ptr()) }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err).context("Failed to configure root path"); + } + } + + { + let virgl_flags = VIRGLRENDERER_USE_EGL + | VIRGLRENDERER_DRM + | VIRGLRENDERER_THREAD_SYNC + | VIRGLRENDERER_USE_ASYNC_FENCE_CB; + // SAFETY: Safe as no pointers involved. + let err = unsafe { krun_set_gpu_options(ctx_id, virgl_flags) }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err).context("Failed to configure gpu"); + } + } + + if options.net == NetMode::PASST { + let passt_fd: OwnedFd = if let Some(passt_socket) = options.passt_socket { + connect_to_passt(passt_socket) + .context("Failed to connect to `passt`")? + .into() + } else { + start_passt().context("Failed to start `passt`")? + }; + // SAFETY: `passt_fd` is an `OwnedFd` and consumed to prevent closing on drop. + // See https://doc.rust-lang.org/std/io/index.html#io-safety + let err = unsafe { krun_set_passt_fd(ctx_id, passt_fd.into_raw_fd()) }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err).context("Failed to configure net mode"); + } + } + + let username = env::var("USER").context("Failed to get username from environment")?; + let user = User::from_name(&username) + .map_err(Into::into) + .and_then(|user| user.ok_or_else(|| anyhow!("requested entry not found"))) + .with_context(|| format!("Failed to get user `{username}` from user database"))?; + let workdir_path = CString::new( + user.dir + .to_str() + .expect("workdir_path should not contain invalid UTF-8"), + ) + .expect("workdir_path should not contain NUL character"); + + { + // Set the working directory to the user's home directory, just for the sake of + // completeness. + // + // SAFETY: `workdir_path` is a pointer to a `CString` with long enough lifetime. + let err = unsafe { krun_set_workdir(ctx_id, workdir_path.as_ptr()) }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err).with_context(|| { + format!( + "Failed to configure `{}` as working directory", + workdir_path + .into_string() + .expect("workdir_path should not contain invalid UTF-8") + ) + }); + } + } + + let krun_guest_path = + find_in_path("krun-guest").context("Failed to check existence of `krun-guest`")?; + let krun_guest_path = if let Some(krun_guest_path) = krun_guest_path { + krun_guest_path + } else { + let krun_path = env::current_exe().and_then(|p| p.canonicalize()); + let krun_path = krun_path.context("Failed to get path of current running executable")?; + krun_path.with_file_name(format!( + "{}-guest", + krun_path + .file_name() + .expect("krun_path should end with a file name") + .to_str() + .context("Failed to process `krun` file name as it contains invalid UTF-8")? + )) + }; + let krun_guest_path = CString::new( + krun_guest_path + .to_str() + .context("Failed to process `krun-guest` path as it contains invalid UTF-8")?, + ) + .context("Failed to process `krun-guest` path as it contains NUL character")?; + + let mut krun_guest_args: Vec = vec![ + CString::new(username).expect("username should not contain NUL character"), + CString::new(format!("{}", getuid().as_raw())) + .expect("uid should not contain NUL character"), + CString::new(format!("{}", getgid().as_raw())) + .expect("gid should not contain NUL character"), + ]; + krun_guest_args.push( + CString::new(options.command) + .context("Failed to process command as it contains NUL character")?, + ); + let command_argc = options.command_args.len(); + for arg in options.command_args { + let s = CString::new(arg) + .context("Failed to process command arg as it contains NUL character")?; + krun_guest_args.push(s); + } + let krun_guest_args: Vec<*const c_char> = { + const KRUN_GUEST_ARGS_FIXED: usize = 4; + // SAFETY: All pointers must be stored in the same allocation. + // See https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html#safety + let mut vec = Vec::with_capacity(KRUN_GUEST_ARGS_FIXED + command_argc + 1); + for s in &krun_guest_args { + vec.push(s.as_ptr()); + } + vec.push(std::ptr::null()); + vec + }; + + let mut env: Vec = vec![]; + + // Automatically pass these environment variables to the microVM, if they are set. + const WELL_KNOWN_ENV_VARS: [&str; 5] = [ + "LD_LIBRARY_PATH", + "LIBGL_DRIVERS_PATH", + "MESA_LOADER_DRIVER_OVERRIDE", // needed for asahi + "PATH", // needed by `krun-guest` program + "RUST_LOG", + ]; + for key in WELL_KNOWN_ENV_VARS { + let value = match env::var(key) { + Ok(value) => value, + Err(VarError::NotPresent) => { + continue; + }, + Err(err) => Err(err).with_context(|| format!("Failed to get `{key}` env var"))?, + }; + let s = CString::new(format!("{key}={value}")).with_context(|| { + format!("Failed to process `{key}` env var as it contains NUL character") + })?; + env.push(s); + } + + for (key, value) in options.env { + let value = value.map_or_else( + || env::var(&key).with_context(|| format!("Failed to get `{key}` env var")), + Ok, + )?; + let s = CString::new(format!("{key}={value}")).with_context(|| { + format!("Failed to process `{key}` env var as it contains NUL character") + })?; + env.push(s); + } + + debug!(env:?; "env vars"); + + let env: Vec<*const c_char> = { + // SAFETY: All pointers must be stored in the same allocation. + // See https://doc.rust-lang.org/std/slice/fn.from_raw_parts.html#safety + let mut vec = Vec::with_capacity(env.len() + 1); + for s in &env { + vec.push(s.as_ptr()); + } + vec.push(std::ptr::null()); + vec + }; + + { + // Specify the path of the binary to be executed in the isolated context, relative to + // the root path. + // + // SAFETY: + // * `krun_guest_path` is a pointer to a `CString` with long enough lifetime. + // * `krun_guest_args` is a pointer to a `Vec` of pointers to `CString`s all with long + // enough lifetime. + // * `env` is a pointer to a `Vec` of pointers to `CString`s all with long enough lifetime. + let err = unsafe { + krun_set_exec( + ctx_id, + krun_guest_path.as_ptr(), + krun_guest_args.as_ptr(), + env.as_ptr(), + ) + }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err) + .context("Failed to configure the parameters for the executable to be run"); + } + } + + { + // Start and enter the microVM. Unless there is some error while creating the microVM + // this function never returns. + // + // SAFETY: Safe as no pointers involved. + let err = unsafe { krun_start_enter(ctx_id) }; + if err < 0 { + let err = Errno::from_raw_os_error(-err); + return Err(err).context("Failed to create the microVM"); + } + } + + unreachable!("`krun_start_enter` should never return"); +} diff --git a/crates/krun/src/cli_options.rs b/crates/krun/src/cli_options.rs new file mode 100644 index 0000000..44b77d2 --- /dev/null +++ b/crates/krun/src/cli_options.rs @@ -0,0 +1,75 @@ +use std::path::PathBuf; + +use anyhow::anyhow; +use bpaf::{any, construct, long, positional, OptionParser, Parser}; + +use crate::net::NetMode; + +#[derive(Clone, Debug)] +pub struct Options { + pub env: Vec<(String, Option)>, + pub net: NetMode, + pub passt_socket: Option, + pub command: String, + pub command_args: Vec, +} + +pub fn options() -> OptionParser { + let env = long("env") + .short('e') + .help( + "Set environment variable to be passed to the microVM + ENV should be in KEY=VALUE format, or KEY on its own to inherit the + current value from the local environment", + ) + .argument::("ENV") + .parse(|s| match s.split_once('=') { + Some(("", _)) => Err(anyhow!("invalid ENV format")), + Some((k, v)) => Ok((k.to_owned(), Some(v.to_owned()))), + None => Ok((s, None)), + }) + .many(); + let net = long("net") + .help( + "Set network mode + NET_MODE can be either PASST (default) or TSI", + ) + .argument::("NET_MODE") + .fallback("PASST".to_owned()) + .display_fallback() + .parse(|s| match &*s.to_ascii_uppercase() { + "PASST" => Ok(NetMode::PASST), + "TSI" => Ok(NetMode::TSI), + _ => Err(anyhow!("invalid NET_MODE value")), + }); + let passt_socket = long("passt-socket") + .help("Instead of starting passt, connect to passt socket at PATH") + .argument("PATH") + .optional(); + let command = positional("COMMAND").help("the command you want to execute in the vm"); + let command_args = any::("COMMAND_ARGS", |arg| { + (!["--help", "-h"].contains(&&*arg)).then_some(arg) + }) + .help("arguments of COMMAND") + .many(); + + construct!(Options { + env, + net, + passt_socket, + // positionals + command, + command_args, + }) + .to_options() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn check_options() { + options().check_invariants(false) + } +} diff --git a/crates/krun/src/lib.rs b/crates/krun/src/lib.rs new file mode 100644 index 0000000..3376b07 --- /dev/null +++ b/crates/krun/src/lib.rs @@ -0,0 +1,2 @@ +pub mod cli_options; +pub mod net; diff --git a/crates/krun/src/net.rs b/crates/krun/src/net.rs new file mode 100644 index 0000000..dff029a --- /dev/null +++ b/crates/krun/src/net.rs @@ -0,0 +1,63 @@ +use std::{ + fmt, + os::{ + fd::{AsRawFd, IntoRawFd, OwnedFd}, + unix::net::UnixStream, + }, + path::Path, + process::Command, +}; + +use anyhow::{Context, Result}; +use log::debug; +use rustix::{ + io::dup, + net::{socketpair, AddressFamily, SocketFlags, SocketType}, +}; + +#[derive(Copy, Clone, Eq, PartialEq, Debug)] +pub enum NetMode { + PASST = 0, + TSI, +} + +pub fn connect_to_passt

(passt_socket_path: P) -> Result +where + P: AsRef + fmt::Debug, +{ + Ok(UnixStream::connect(passt_socket_path)?) +} + +pub fn start_passt() -> Result { + let (parent_fd, child_fd) = socketpair( + AddressFamily::UNIX, + SocketType::STREAM, + // SAFETY: The child process should not inherit `parent_fd`. + SocketFlags::CLOEXEC, + None, + )?; + + // SAFETY: The parent process should not keep `child_fd` open. It is an `OwnedFd` so it will be + // closed on drop. + // See https://doc.rust-lang.org/std/io/index.html#io-safety + // + // The `dup` call clears the `FD_CLOEXEC` flag on the new `child_fd`, which should be inherited + // by the child process. + let child_fd = + dup(child_fd).context("Failed to duplicate file descriptor for `passt` child process")?; + + debug!(fd = child_fd.as_raw_fd(); "passing fd to passt"); + + // SAFETY: `child_fd` is an `OwnedFd` and consumed to prevent closing on drop, as it will now be + // owned by the child process. + // See https://doc.rust-lang.org/std/io/index.html#io-safety + let child = Command::new("passt") + .args(["-q", "-f", "--fd"]) + .arg(format!("{}", child_fd.into_raw_fd())) + .spawn(); + if let Err(err) = child { + return Err(err).context("Failed to execute `passt` as child process"); + } + + Ok(parent_fd) +} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml new file mode 100644 index 0000000..ba20df6 --- /dev/null +++ b/crates/utils/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "utils" +authors = { workspace = true } +edition = { workspace = true } +rust-version = { workspace = true } +repository = { workspace = true } +license = { workspace = true } +publish = false + +[features] +default = [] diff --git a/crates/utils/src/env.rs b/crates/utils/src/env.rs new file mode 100644 index 0000000..8497cd6 --- /dev/null +++ b/crates/utils/src/env.rs @@ -0,0 +1,39 @@ +use std::{ + env, fs, io, + os::unix::fs::PermissionsExt as _, + path::{Path, PathBuf}, +}; + +pub fn find_in_path

(program: P) -> io::Result> +where + P: AsRef, +{ + let program = program.as_ref(); + + // Only accept program name, i.e. a relative path with one component. + if program.parent() != Some(Path::new("")) { + return Err(io::Error::other(format!( + "invalid program name {program:?}" + ))); + }; + + // Impossible to perform search if `PATH` env var is not set or invalid. + let Ok(path_env) = env::var("PATH") else { + return Err(io::Error::other("`PATH` env var is not set or invalid")); + }; + + for search_path in env::split_paths(&path_env) { + let pb = search_path.join(program); + if !pb.is_file() { + continue; + } + let Ok(metadata) = fs::metadata(&pb) else { + continue; + }; + if metadata.permissions().mode() & 0o111 != 0 { + return pb.canonicalize().map(Some); + } + } + + Ok(None) +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs new file mode 100644 index 0000000..3d7924f --- /dev/null +++ b/crates/utils/src/lib.rs @@ -0,0 +1 @@ +pub mod env; diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..bba0995 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,18 @@ +edition = "2021" + +# empty_item_single_line = false +# error_on_line_overflow = true +# format_code_in_doc_comments = true +# format_strings = true +# group_imports = "StdExternalCrate" +# imports_granularity = "Crate" +# imports_layout = "HorizontalVertical" +match_block_trailing_comma = true +newline_style = "Unix" +# normalize_comments = true +# normalize_doc_attributes = true +# overflow_delimited_expr = true +# reorder_impl_items = true +use_field_init_shorthand = true +use_try_shorthand = true +# wrap_comments = true