diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1c9a337f5..b2925d4ac 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -66,6 +66,26 @@ jobs: tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} + runtimes: + runs-on: ubuntu-latest + needs: + - lints + - linux + - mac + - freebsd + env: + AWS_REGION: eu-west-1 + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + steps: + - uses: actions/checkout@v4 + - name: Installing dependencies + run: sudo ./scripts/deps.sh ubuntu:latest + - name: Installing Rust + run: ./scripts/rust.sh 1.70 + - name: Generating runtimes + run: make runtimes + docs: runs-on: ubuntu-latest needs: diff --git a/Cargo.lock b/Cargo.lock index 25f0bfd26..9d31f5df7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -56,6 +56,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c79fed4cdb43e993fcdadc7e58a09fd0e3e649c4436fa11da71c9f1f3ee7feb9" + [[package]] name = "bitflags" version = "1.3.2" @@ -148,6 +154,15 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" @@ -210,12 +225,43 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "windows-sys 0.52.0", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +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 = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -252,13 +298,26 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "inko" version = "0.13.2" dependencies = [ "compiler", + "flate2", "getopts", + "tar", "types", + "ureq", ] [[package]] @@ -386,6 +445,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -506,6 +571,20 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "rt" version = "0.13.2" @@ -540,12 +619,44 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "scopeguard" version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "semver" version = "1.0.20" @@ -588,6 +699,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "subtle" version = "2.5.0" @@ -605,6 +722,32 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[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 = "typenum" version = "1.17.0" @@ -615,12 +758,27 @@ checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" name = "types" version = "0.13.2" +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.10.1" @@ -633,6 +791,39 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +dependencies = [ + "base64", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-webpki", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "version_check" version = "0.9.4" @@ -645,6 +836,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "windows-sys" version = "0.45.0" @@ -663,6 +860,15 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + [[package]] name = "windows-targets" version = "0.42.2" @@ -693,6 +899,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -705,6 +926,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -717,6 +944,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -729,6 +962,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -741,6 +980,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -753,6 +998,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -765,6 +1016,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -776,3 +1033,18 @@ name = "windows_x86_64_msvc" version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "xattr" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbc6ab6ec1907d1a901cdbcd2bd4cb9e7d64ce5c9739cbb97d3c391acd8c7fae" +dependencies = [ + "libc", +] diff --git a/Makefile b/Makefile index fe1b34f58..248ee1f0a 100644 --- a/Makefile +++ b/Makefile @@ -165,18 +165,10 @@ docs/versions: --distribution-id ${DOCS_CLOUDFRONT_ID} --paths "/manual/versions.json" rm versions.json -clippy: - touch */src/lib.rs */src/main.rs - cargo clippy -- -D warnings - -rustfmt-check: - rustfmt --check */src/lib.rs */src/main.rs - -rustfmt: - rustfmt --emit files */src/lib.rs */src/main.rs +runtimes: + bash scripts/runtimes.sh ${VERSION} .PHONY: release/source release/manifest release/changelog release/versions .PHONY: release/commit release/publish release/tag -.PHONY: build install clean -.PHONY: rustfmt rustfmt-check clippy +.PHONY: build install clean runtimes .PHONY: docs/install docs/build docs/server docs/publish docs/versions diff --git a/compiler/src/config.rs b/compiler/src/config.rs index a2b0f0e8f..5d4915efa 100644 --- a/compiler/src/config.rs +++ b/compiler/src/config.rs @@ -29,6 +29,29 @@ const MAIN_TEST_MODULE: &str = "inko-tests"; /// The name of the directory to store build files in. const BUILD: &str = "build"; +fn home_dir() -> Option { + env::var_os("HOME").filter(|v| !v.is_empty()).map(PathBuf::from) +} + +pub fn data_directory() -> Option { + let base = if cfg!(target_os = "macos") { + home_dir().map(|h| h.join("Library").join("Application Support")) + } else { + env::var_os("XDG_DATA_HOME") + .filter(|v| !v.is_empty()) + .map(PathBuf::from) + .or_else(|| home_dir().map(|h| h.join(".local").join("share"))) + }; + + base.map(|p| p.join("inko")) +} + +pub fn local_runtimes_directory() -> Option { + // The Inko ABI isn't stable, so runtimes are scoped to the Inko version + // they were compiled for. + data_directory().map(|p| p.join("runtimes").join(env!("CARGO_PKG_VERSION"))) +} + fn create_directory(path: &Path) -> Result<(), String> { if path.is_dir() { return Ok(()); @@ -134,7 +157,7 @@ pub enum Output { } /// A type describing which linker to use. -#[derive(Copy, Clone)] +#[derive(Clone)] pub enum Linker { /// Detect which linker to use. Detect, @@ -147,6 +170,12 @@ pub enum Linker { /// Always use Mold. Mold, + + /// Always use Zig. + Zig, + + /// Use a custom linker with any extra arguments. + Custom(String), } impl Linker { @@ -155,9 +184,15 @@ impl Linker { "system" => Some(Linker::System), "lld" => Some(Linker::Lld), "mold" => Some(Linker::Mold), + "zig" => Some(Linker::Zig), + _ if !value.is_empty() => Some(Linker::Custom(value.to_string())), _ => None, } } + + pub(crate) fn is_zig(&self) -> bool { + matches!(self, Linker::Zig) + } } /// A type for storing compiler configuration, such as the source directories to @@ -166,8 +201,7 @@ pub struct Config { /// The directory containing the Inko's standard library. pub(crate) std: PathBuf, - /// The directory containing runtime library files to link to the generated - /// code. + /// The path to the global runtime directory. pub runtime: PathBuf, /// The directory containing the project's source code. @@ -219,6 +253,9 @@ pub struct Config { /// The linker to use. pub linker: Linker, + /// Extra arguments to pass to the linker. + pub linker_arguments: Vec, + /// If incremental compilation is enabled or not. pub incremental: bool, @@ -259,6 +296,7 @@ impl Config { static_linking: false, threads: available_parallelism().map(|v| v.get()).unwrap_or(1), linker: Linker::Detect, + linker_arguments: Vec::new(), incremental: true, compiled_at, } @@ -286,7 +324,7 @@ impl Config { } pub fn set_target(&mut self, name: &str) -> Result<(), String> { - if let Some(val) = Target::from_str(name) { + if let Some(val) = Target::parse(name) { self.target = val; Ok(()) } else { diff --git a/compiler/src/linker.rs b/compiler/src/linker.rs index e308c2508..39d20ceca 100644 --- a/compiler/src/linker.rs +++ b/compiler/src/linker.rs @@ -1,33 +1,15 @@ -use crate::config::{Config, Linker}; +use crate::config::{local_runtimes_directory, Linker}; use crate::state::State; -use crate::target::OperatingSystem; +use crate::target::{OperatingSystem, Target}; +use std::io::Read as _; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; -fn runtime_library(config: &Config) -> Option { - let mut files = vec![format!("libinko-{}.a", &config.target)]; - - // When compiling for the native target we also support DIR/libinko.a, as - // this makes development of Inko easier by just using e.g. `./target/debug` - // as the search directory. - if config.target.is_native() { - files.push("libinko.a".to_string()); - } - - files.iter().find_map(|file| { - let path = config.runtime.join(file); - - if path.is_file() { - Some(path) - } else { - None - } - }) -} - -fn linker_is_available(linker: &str) -> bool { - Command::new(linker) - .arg("--version") +fn command_is_available(name: &str) -> bool { + // We use --help here instead of --version, as not all commands may have a + // --version flag (e.g. "zig" for some reason). + Command::new(name) + .arg("--help") .stdout(Stdio::null()) .stderr(Stdio::null()) .stdin(Stdio::null()) @@ -36,12 +18,169 @@ fn linker_is_available(linker: &str) -> bool { .map_or(false, |status| status.success()) } +fn cc_is_clang() -> bool { + let Ok(mut child) = Command::new("cc") + .arg("--version") + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .stdin(Stdio::null()) + .spawn() + else { + return false; + }; + + let mut stdout = child.stdout.take().unwrap(); + let Ok(status) = child.wait() else { return false }; + let mut output = String::new(); + let _ = stdout.read_to_string(&mut output); + + status.success() && output.contains("clang version") +} + fn lld_is_available() -> bool { - linker_is_available("ld.lld") + command_is_available("ld.lld") } fn mold_is_available() -> bool { - linker_is_available("ld.mold") + command_is_available("ld.mold") +} + +fn musl_linker(target: &Target) -> Option<&'static str> { + if !target.abi.is_musl() { + return None; + } + + let gcc = "musl-gcc"; + let clang = "musl-clang"; + + if command_is_available(gcc) { + Some(gcc) + } else if command_is_available(clang) { + Some(clang) + } else { + None + } +} + +fn zig_cc(target: &Target) -> Command { + let mut cmd = Command::new("zig"); + + // To make using Zig as a linker a bit easier, we translate our target + // triples into those used by Zig (which in turn are a bit different + // from the ones used by LLVM). + cmd.arg("cc"); + cmd.arg(&format!("--target={}", target.zig_triple())); + cmd +} + +fn driver(state: &State) -> Result { + let target = &state.config.target; + let triple = target.llvm_triple(); + let cmd = if let Linker::Custom(name) = &state.config.linker { + Command::new(name) + } else if state.config.linker.is_zig() { + zig_cc(target) + } else { + let gcc_exe = format!("{}-gcc", triple); + let mut linker = state.config.linker.clone(); + let mut cmd = if target.is_native() { + Command::new("cc") + } else if target.os.is_mac() && command_is_available("zig") { + // Cross-compiling from a non-mac host to macOS is a pain due to the + // licensing of the various dependencies needed for this. Zig makes + // this much easier, so we'll use it if it's available. + linker = Linker::System; + zig_cc(target) + } else if command_is_available(&gcc_exe) { + // GCC cross compilers don't support the `-fuse-ld` flag, so in this + // case we force the use of the system linker. + linker = Linker::System; + Command::new(gcc_exe) + } else if let Some(name) = musl_linker(target) { + // For musl we want to use the musl-gcc/musl-clang wrappers so we + // don't have to figure out all the necessary parameters ourselves. + linker = Linker::System; + Command::new(name) + } else if cc_is_clang() || command_is_available("clang") { + // We check for clang _after_ GCC, because if a dedicated GCC + // executable for the target is available, using it is less prone to + // error as we don't have to bother finding the right sysroot. + Command::new("clang") + } else { + return Err(format!( + "you are cross-compiling to {}, but the linker used (cc) \ + doesn't support cross-compilation. You can specify a custom \ + linker using the --linker=LINKER option", + triple + )); + }; + + if cmd.get_program() == "clang" { + // clang tends to pick the host version of any necessary libraries + // (including crt1.o and the likes) when cross-compiling. We try to + // fix this here by automatically setting the correct sysroot. + // + // Linux distributions (Arch Linux, Fedora, Ubuntu, etc) typically + // install the toolchains in /usr, e.g. /usr/aarch64-linux-gnu. + // + // For other platforms we don't bother trying to find the sysroot, + // as they don't reside in a consistent location. + if cfg!(target_os = "linux") { + let path = format!("/usr/{}", triple); + + if Path::new(&path).is_dir() { + cmd.arg(format!("--sysroot={}", path)); + } + } + + cmd.arg(&format!("--target={}", triple)); + } + + if let Linker::Detect = linker { + // Mold doesn't support macOS, so we don't enable it for macOS + // targets. + if mold_is_available() && !target.os.is_mac() { + linker = Linker::Mold; + } else if lld_is_available() { + linker = Linker::Lld; + } + } + + match linker { + Linker::Lld => { + cmd.arg("-fuse-ld=lld"); + } + Linker::Mold => { + cmd.arg("-fuse-ld=mold"); + } + _ => {} + } + + cmd + }; + + let name = cmd.get_program(); + + // While it's possible the user has specified the many flags needed to get + // regular clang/gcc to correctly use musl, it's rather unlikely, so we + // instead just direct users to use musl-clang, musl-gcc, or zig and be done + // with it. + if cfg!(target_env = "gnu") + && target.abi.is_musl() + && name != "musl-clang" + && name != "musl-gcc" + && name != "zig" + { + return Err( + "targeting musl on a GNU host using clang or gcc is likely to \ + result in the executable still linking against glibc. To resolve \ + this, use --linker=zig, or --linker=musl-clang/--linker=musl-gcc \ + (provided musl-clang/musl-gcc is in your PATH)" + .to_string(), + ); + } + + Ok(cmd) } pub(crate) fn link( @@ -49,14 +188,11 @@ pub(crate) fn link( output: &Path, paths: &[PathBuf], ) -> Result<(), String> { - // On Unix systems the necessary libraries/object files are all over the - // place. Instead of re-implementing the logic necessary to find these - // files, we rely on the system's compiler to do this for us. - // - // As we only use this executable for linking it doesn't really matter - // if this ends up using gcc, clang or something else, because we only - // use it as a wrapper around the linker executable. - let mut cmd = Command::new("cc"); + let mut cmd = driver(state)?; + + for arg in &state.config.linker_arguments { + cmd.arg(arg); + } // Object files must come before any of the libraries to link against, as // certain linkers are very particular about the order of flags such as @@ -65,11 +201,33 @@ pub(crate) fn link( cmd.arg(path); } - let rt_path = runtime_library(&state.config).ok_or_else(|| { - format!("No runtime is available for target '{}'", state.config.target) - })?; + if state.config.target.is_native() { + cmd.arg(state.config.runtime.join("libinko.a")); + } else if let Some(runtimes) = local_runtimes_directory() { + let dir = runtimes.join(state.config.target.to_string()); + let inko = dir.join("libinko.a"); + let unwind = dir.join("libunwind.a"); - cmd.arg(&rt_path); + if !inko.is_file() { + return Err(format!( + "no runtime is available for target '{}'", + state.config.target + )); + } + + cmd.arg(inko); + + // On musl hosts we just rely on whatever the system unwinder is. This + // way distributions such as Alpine don't need to patch things out to + // achieve that. On other hosts we use the bundled libunwind, otherwise + // we get _Unwind_XXX linker errors. + if !cfg!(target_env = "musl") + && state.config.target.abi.is_musl() + && unwind.is_file() + { + cmd.arg(unwind); + } + } // Include any extra platform specific libraries, such as libm on the // various Unix platforms. These must come _after_ any object files and @@ -87,7 +245,7 @@ pub(crate) fn link( OperatingSystem::Linux => { // Certain versions of Linux (e.g. Debian 11) also need libdl and // libpthread to be linked in explicitly. We use the --as-needed - // flag here (supported by both gcc and clang) to only link these + // flag here (supported by both GCC and clang) to only link these // libraries if actually needed. cmd.arg("-Wl,--as-needed"); cmd.arg("-ldl"); @@ -103,6 +261,19 @@ pub(crate) fn link( let mut static_linking = state.config.static_linking; + if !cfg!(target_env = "musl") && state.config.target.abi.is_musl() { + // If a non-musl hosts targets musl, we statically link everything. The + // reason for this is that when using musl one might believe the + // resulting executable to be portable, but that's only the case if it's + // indeed a statically linked executable. + // + // This does mean any C dependencies need to be available in their + // static form, but if that's not the case then targeting musl on + // non-musl hosts isn't going to work well anyway (e.g. because the + // dynamic libraries are likely to link to glibc). + static_linking = true; + } + match state.config.target.os { OperatingSystem::Mac if static_linking => { // On macOS there's no equivalent of -l:libX.a as there is for GNU @@ -118,10 +289,6 @@ pub(crate) fn link( _ => (), } - if static_linking { - cmd.arg("-Wl,-Bstatic"); - } - for lib in &state.libraries { // These libraries are already included if needed, and we can't // statically link against them (if static linking is desired), so we @@ -130,41 +297,41 @@ pub(crate) fn link( continue; } - cmd.arg(&(format!("-l{}", lib))); - } + // We don't use the pattern `-Wl,-Bstatic -lX -Wl,-Bdynamic` as the + // "closing" `-Bdynamic` also affects any linker flags that come after + // it, which can prevent us from static linking against e.g. libc for + // musl targets. + let flag = if static_linking { + format!("-l:lib{}.a", lib) + } else { + format!("-l{}", lib) + }; - if static_linking { - cmd.arg("-Wl,-Bdynamic"); + cmd.arg(&flag); } - cmd.arg("-o"); - cmd.arg(output); + if state.config.target.os.is_linux() { + // For these targets we need to ensure this flag is set, which isn't + // always passed by GCC (and possibly other) compilers. + cmd.arg("-Wl,--eh-frame-hdr"); - if let OperatingSystem::Linux = state.config.target.os { // This removes the need for installing libgcc in deployment // environments. cmd.arg("-static-libgcc"); } - let mut linker = state.config.linker; - - if let Linker::Detect = linker { - if mold_is_available() { - linker = Linker::Mold; - } else if lld_is_available() { - linker = Linker::Lld; - } + // In case we're targeting musl we also want to statically link musl's libc. + // This isn't done for GNU targets because glibc makes use of dlopen(), so + // static linking glibc is basically a lie (and generally recommended + // against). This also ensures all linkers behave the same, as e.g. Zig + // defaults to static linking (https://github.com/ziglang/zig/issues/11909) + // but musl-clang and musl-gcc default to dynamic linking. + if static_linking && state.config.target.abi.is_musl() { + cmd.arg("-static"); } - match linker { - Linker::Lld => { - cmd.arg("-fuse-ld=lld"); - } - Linker::Mold => { - cmd.arg("-fuse-ld=mold"); - } - _ => {} - } + cmd.arg("-o"); + cmd.arg(output); cmd.stdin(Stdio::null()); cmd.stderr(Stdio::piped()); @@ -182,9 +349,9 @@ pub(crate) fn link( Ok(()) } else { Err(format!( - "The linker exited with status code {}:\n{}", + "the linker exited with status code {}:\n\n{}", output.status.code().unwrap_or(0), - String::from_utf8_lossy(&output.stderr), + String::from_utf8_lossy(&output.stderr).trim(), )) } } diff --git a/compiler/src/target.rs b/compiler/src/target.rs index c4443da22..1c2319294 100644 --- a/compiler/src/target.rs +++ b/compiler/src/target.rs @@ -56,6 +56,14 @@ impl OperatingSystem { panic!("The host operating system isn't supported"); } } + + pub(crate) fn is_mac(&self) -> bool { + matches!(self, OperatingSystem::Mac) + } + + pub(crate) fn is_linux(&self) -> bool { + matches!(self, OperatingSystem::Linux) + } } /// The ABI to target. @@ -85,6 +93,10 @@ impl Abi { Abi::Native } } + + pub(crate) fn is_musl(&self) -> bool { + matches!(self, Abi::Musl) + } } /// A type describing the compile target, such as the operating system and @@ -97,10 +109,51 @@ pub struct Target { } impl Target { + /// Returns a list of all the targets we officially support. + pub fn supported() -> Vec { + use Abi::*; + use Architecture::*; + use OperatingSystem::*; + + vec![ + Target::new(Amd64, Freebsd, Native), + Target::new(Amd64, Linux, Gnu), + Target::new(Amd64, Linux, Musl), + Target::new(Arm64, Linux, Gnu), + Target::new(Arm64, Linux, Musl), + Target::new(Amd64, Mac, Native), + Target::new(Arm64, Mac, Native), + ] + } + + /// Returns a list of the targets for which a pre-built runtime is provided. + pub fn runtimes() -> Vec { + use Abi::*; + use Architecture::*; + use OperatingSystem::*; + + vec![ + Target::new(Amd64, Linux, Gnu), + Target::new(Amd64, Linux, Musl), + Target::new(Arm64, Linux, Gnu), + Target::new(Arm64, Linux, Musl), + Target::new(Amd64, Mac, Native), + Target::new(Arm64, Mac, Native), + ] + } + + pub(crate) fn new( + arch: Architecture, + os: OperatingSystem, + abi: Abi, + ) -> Target { + Target { arch, os, abi } + } + /// Parses a target from a string. /// /// If the target is invalid, a None is returned. - pub(crate) fn from_str(input: &str) -> Option { + pub fn parse(input: &str) -> Option { let mut iter = input.split('-'); let arch = iter.next().and_then(Architecture::from_str)?; let os = iter.next().and_then(OperatingSystem::from_str)?; @@ -118,6 +171,10 @@ impl Target { } } + pub fn runtime_file_name(&self) -> String { + format!("libinko-{}.a", self) + } + /// Returns a String describing the target using the LLVM triple format. pub(crate) fn llvm_triple(&self) -> String { let arch = match self.arch { @@ -128,13 +185,25 @@ impl Target { let os = match self.os { OperatingSystem::Freebsd => "unknown-freebsd", OperatingSystem::Mac => "apple-darwin", - OperatingSystem::Linux => { - if let Abi::Musl = self.abi { - "unknown-linux-musl" - } else { - "unknown-linux-gnu" - } - } + OperatingSystem::Linux if self.abi.is_musl() => "linux-musl", + OperatingSystem::Linux => "linux-gnu", + }; + + format!("{}-{}", arch, os) + } + + /// Returns a String describing the target using Zig's triple format. + pub(crate) fn zig_triple(&self) -> String { + let arch = match self.arch { + Architecture::Amd64 => "x86_64", + Architecture::Arm64 => "aarch64", + }; + + let os = match self.os { + OperatingSystem::Freebsd => "freebsd-none", + OperatingSystem::Mac => "macos-none", + OperatingSystem::Linux if self.abi.is_musl() => "linux-musl", + OperatingSystem::Linux => "linux-gnu", }; format!("{}-{}", arch, os) @@ -172,7 +241,7 @@ impl Target { } } - pub(crate) fn is_native(&self) -> bool { + pub fn is_native(&self) -> bool { self == &Target::native() } @@ -204,10 +273,6 @@ impl fmt::Display for Target { mod tests { use super::*; - fn target(arch: Architecture, os: OperatingSystem, abi: Abi) -> Target { - Target { arch, os, abi } - } - #[test] fn test_operating_system_from_str() { assert_eq!( @@ -235,29 +300,33 @@ mod tests { #[test] fn test_target_from_str() { assert_eq!( - Target::from_str("amd64-freebsd-native"), - Some(target( + Target::parse("amd64-freebsd-native"), + Some(Target::new( Architecture::Amd64, OperatingSystem::Freebsd, Abi::Native )) ); assert_eq!( - Target::from_str("arm64-linux-gnu"), - Some(target(Architecture::Arm64, OperatingSystem::Linux, Abi::Gnu)) + Target::parse("arm64-linux-gnu"), + Some(Target::new( + Architecture::Arm64, + OperatingSystem::Linux, + Abi::Gnu + )) ); assert_eq!( - Target::from_str("arm64-linux-musl"), - Some(target( + Target::parse("arm64-linux-musl"), + Some(Target::new( Architecture::Arm64, OperatingSystem::Linux, Abi::Musl )) ); - assert_eq!(Target::from_str("bla-linux-native"), None); - assert_eq!(Target::from_str("amd64-bla-native"), None); - assert_eq!(Target::from_str("amd64-linux"), None); + assert_eq!(Target::parse("bla-linux-native"), None); + assert_eq!(Target::parse("amd64-bla-native"), None); + assert_eq!(Target::parse("amd64-linux"), None); } #[test] @@ -271,46 +340,94 @@ mod tests { #[test] fn test_target_llvm_triple() { assert_eq!( - target(Architecture::Amd64, OperatingSystem::Linux, Abi::Native) - .llvm_triple(), - "x86_64-unknown-linux-gnu" + Target::new( + Architecture::Amd64, + OperatingSystem::Linux, + Abi::Native + ) + .llvm_triple(), + "x86_64-linux-gnu" ); assert_eq!( - target(Architecture::Amd64, OperatingSystem::Linux, Abi::Musl) + Target::new(Architecture::Amd64, OperatingSystem::Linux, Abi::Musl) .llvm_triple(), - "x86_64-unknown-linux-musl" + "x86_64-linux-musl" ); assert_eq!( - target(Architecture::Amd64, OperatingSystem::Freebsd, Abi::Native) - .llvm_triple(), + Target::new( + Architecture::Amd64, + OperatingSystem::Freebsd, + Abi::Native + ) + .llvm_triple(), "x86_64-unknown-freebsd" ); assert_eq!( - target(Architecture::Arm64, OperatingSystem::Mac, Abi::Native) + Target::new(Architecture::Arm64, OperatingSystem::Mac, Abi::Native) .llvm_triple(), "aarch64-apple-darwin" ); } + #[test] + fn test_target_zig_triple() { + assert_eq!( + Target::new( + Architecture::Amd64, + OperatingSystem::Linux, + Abi::Native + ) + .zig_triple(), + "x86_64-linux-gnu" + ); + assert_eq!( + Target::new(Architecture::Amd64, OperatingSystem::Linux, Abi::Musl) + .zig_triple(), + "x86_64-linux-musl" + ); + assert_eq!( + Target::new( + Architecture::Amd64, + OperatingSystem::Freebsd, + Abi::Native + ) + .zig_triple(), + "x86_64-freebsd-none" + ); + assert_eq!( + Target::new(Architecture::Arm64, OperatingSystem::Mac, Abi::Native) + .zig_triple(), + "aarch64-macos-none" + ); + } + #[test] fn test_target_to_string() { assert_eq!( - target(Architecture::Amd64, OperatingSystem::Linux, Abi::Native) - .to_string(), + Target::new( + Architecture::Amd64, + OperatingSystem::Linux, + Abi::Native + ) + .to_string(), "amd64-linux-gnu" ); assert_eq!( - target(Architecture::Amd64, OperatingSystem::Linux, Abi::Musl) + Target::new(Architecture::Amd64, OperatingSystem::Linux, Abi::Musl) .to_string(), "amd64-linux-musl" ); assert_eq!( - target(Architecture::Amd64, OperatingSystem::Freebsd, Abi::Native) - .to_string(), + Target::new( + Architecture::Amd64, + OperatingSystem::Freebsd, + Abi::Native + ) + .to_string(), "amd64-freebsd-native" ); assert_eq!( - target(Architecture::Arm64, OperatingSystem::Mac, Abi::Native) + Target::new(Architecture::Arm64, OperatingSystem::Mac, Abi::Native) .to_string(), "arm64-mac-native" ); diff --git a/inko/Cargo.toml b/inko/Cargo.toml index eab34d378..0a89f77a2 100644 --- a/inko/Cargo.toml +++ b/inko/Cargo.toml @@ -9,3 +9,6 @@ license = "MPL-2.0" getopts = "^0.2" compiler = { path = "../compiler" } types = { path = "../types" } +ureq = "^2.9" +flate2 = "^1.0" +tar = "^0.4" diff --git a/inko/src/command.rs b/inko/src/command.rs index 6f4f79108..4a7a35e3a 100644 --- a/inko/src/command.rs +++ b/inko/src/command.rs @@ -4,4 +4,6 @@ pub(crate) mod main; pub(crate) mod pkg; pub(crate) mod print; pub(crate) mod run; +pub(crate) mod runtime; +pub(crate) mod targets; pub(crate) mod test; diff --git a/inko/src/command/build.rs b/inko/src/command/build.rs index 3a4f8c951..79c459d91 100644 --- a/inko/src/command/build.rs +++ b/inko/src/command/build.rs @@ -90,8 +90,15 @@ pub(crate) fn run(arguments: &[String]) -> Result { options.optopt( "", "linker", - "The specific linker to use, instead of detecting the linker automatically", - "system,lld,mold", + "A custom linker to use, instead of detecting the linker automatically", + "LINKER", + ); + + options.optmulti( + "", + "linker-arg", + "An extra argument to pass to the linker", + "ARG", ); options.optflag( @@ -152,7 +159,7 @@ pub(crate) fn run(arguments: &[String]) -> Result { if let Some(val) = matches.opt_str("threads") { match val.parse::() { Ok(0) | Err(_) => { - return Err(Error::generic(format!( + return Err(Error::from(format!( "'{}' isn't a valid number of threads", val ))); @@ -163,16 +170,17 @@ pub(crate) fn run(arguments: &[String]) -> Result { if let Some(val) = matches.opt_str("linker") { config.linker = Linker::parse(&val).ok_or_else(|| { - Error::generic(format!("'{}' isn't a valid linker", val)) + Error::from(format!("'{}' isn't a valid linker", val)) })?; } + for arg in matches.opt_strs("linker-arg") { + config.linker_arguments.push(arg); + } + let timings = match matches.opt_str("timings") { Some(val) => Timings::parse(&val).ok_or_else(|| { - Error::generic(format!( - "'{}' is an invalid --timings argument", - val - )) + Error::from(format!("'{}' is an invalid --timings argument", val)) })?, _ if matches.opt_present("timings") => Timings::Basic, _ => Timings::None, @@ -193,6 +201,6 @@ pub(crate) fn run(arguments: &[String]) -> Result { match result { Ok(_) => Ok(0), Err(CompileError::Invalid) => Ok(1), - Err(CompileError::Internal(msg)) => Err(Error::generic(msg)), + Err(CompileError::Internal(msg)) => Err(Error::from(msg)), } } diff --git a/inko/src/command/check.rs b/inko/src/command/check.rs index ce2d4a943..af9f140b7 100644 --- a/inko/src/command/check.rs +++ b/inko/src/command/check.rs @@ -63,6 +63,6 @@ pub(crate) fn run(arguments: &[String]) -> Result { match result { Ok(_) => Ok(0), Err(CompileError::Invalid) => Ok(1), - Err(CompileError::Internal(msg)) => Err(Error::generic(msg)), + Err(CompileError::Internal(msg)) => Err(Error::from(msg)), } } diff --git a/inko/src/command/main.rs b/inko/src/command/main.rs index 6af43382b..92d6b04da 100644 --- a/inko/src/command/main.rs +++ b/inko/src/command/main.rs @@ -3,6 +3,8 @@ use crate::command::check; use crate::command::pkg; use crate::command::print; use crate::command::run; +use crate::command::runtime; +use crate::command::targets; use crate::command::test; use crate::error::Error; use crate::options::print_usage; @@ -13,12 +15,14 @@ const USAGE: &str = "Usage: inko [OPTIONS] [COMMAND | FILE] Commands: - build Compile Inko source code - check Check a project or single file for correctness - pkg Manage Inko packages - print Print compiler details to STDOUT - run Compile and run source code directly - test Run Inko unit tests + build Compile Inko source code + check Check a project or single file for correctness + pkg Manage Inko packages + print Print compiler details to STDOUT + run Compile and run source code directly + runtime Manage runtimes for the available targets + targets List the supported target triples + test Run Inko unit tests Examples: @@ -54,8 +58,10 @@ pub(crate) fn run() -> Result { Some("test") => test::run(&matches.free[1..]), Some("print") => print::run(&matches.free[1..]), Some("pkg") => pkg::run(&matches.free[1..]), + Some("runtime") => runtime::run(&matches.free[1..]), + Some("targets") => targets::run(&matches.free[1..]), Some(cmd) => { - Err(Error::generic(format!("The command '{}' is invalid", cmd))) + Err(Error::from(format!("The command '{}' is invalid", cmd))) } None => { print_usage(&options, USAGE); diff --git a/inko/src/command/pkg.rs b/inko/src/command/pkg.rs index dfd15ce11..b92cbe8da 100644 --- a/inko/src/command/pkg.rs +++ b/inko/src/command/pkg.rs @@ -42,7 +42,7 @@ pub(crate) fn run(arguments: &[String]) -> Result { Some("sync") => sync::run(&matches.free[1..]), Some("update") => update::run(&matches.free[1..]), Some(cmd) => { - Err(Error::generic(format!("The command {:?} is invalid", cmd))) + Err(Error::from(format!("The command {:?} is invalid", cmd))) } None => { print_usage(&options, USAGE); diff --git a/inko/src/command/pkg/add.rs b/inko/src/command/pkg/add.rs index 1e9eb2cb9..910b56c56 100644 --- a/inko/src/command/pkg/add.rs +++ b/inko/src/command/pkg/add.rs @@ -30,19 +30,20 @@ pub(crate) fn run(args: &[String]) -> Result { } if matches.free.len() != 2 { - return Err(Error::generic( + return Err(Error::from( "You must specify a package and version to add".to_string(), )); } - let url = matches.free.first().and_then(|uri| Url::parse(uri)).ok_or_else( - || Error::generic("The package URL is invalid".to_string()), - )?; + let url = + matches.free.first().and_then(|uri| Url::parse(uri)).ok_or_else( + || Error::from("The package URL is invalid".to_string()), + )?; let name = url.import_name(); let version = matches.free.get(1).and_then(|uri| Version::parse(uri)).ok_or_else( - || Error::generic("The package version is invalid".to_string()), + || Error::from("The package version is invalid".to_string()), )?; let dir = data_dir()?.join(url.directory_name()); @@ -68,7 +69,7 @@ pub(crate) fn run(args: &[String]) -> Result { }; let hash = tag.map(|t| t.target).ok_or_else(|| { - Error::generic(format!("Version {} doesn't exist", version)) + Error::from(format!("Version {} doesn't exist", version)) })?; let checksum = Checksum::new(hash); diff --git a/inko/src/command/pkg/update.rs b/inko/src/command/pkg/update.rs index 7be4a8f2e..2b0bfdf9f 100644 --- a/inko/src/command/pkg/update.rs +++ b/inko/src/command/pkg/update.rs @@ -43,7 +43,7 @@ pub(crate) fn run(args: &[String]) -> Result { if let Some(dep) = manifest.find_dependency(&url) { vec![dep] } else { - return Err(Error::generic(format!( + return Err(Error::from(format!( "The package {} isn't listed in {}", url, MANIFEST_FILE ))); @@ -66,7 +66,7 @@ pub(crate) fn run(args: &[String]) -> Result { let tag_names = repo.version_tag_names(); if tag_names.is_empty() { - return Err(Error::generic(format!( + return Err(Error::from(format!( "The package {} doesn't have any versions", dep.url ))); diff --git a/inko/src/command/print.rs b/inko/src/command/print.rs index f94fb1a6a..b817cf35b 100644 --- a/inko/src/command/print.rs +++ b/inko/src/command/print.rs @@ -1,6 +1,6 @@ use crate::error::Error; use crate::options::print_usage; -use compiler::config::Config; +use compiler::config::{local_runtimes_directory, Config}; use compiler::target::Target; use getopts::Options; @@ -11,7 +11,7 @@ Print compiler details, such as the target, to STDOUT. Available values: target # Print the host's target triple (e.g. amd64-linux-gnu) - runtime # Print the path to the static runtime library + runtimes # Print the paths to search for the runtime library Examples: @@ -34,15 +34,19 @@ pub(crate) fn run(arguments: &[String]) -> Result { println!("{}", Target::native()); Ok(0) } - Some("runtime") => { + Some("runtimes") => { + if let Some(dir) = local_runtimes_directory() { + println!("{}", dir.display()); + } + println!("{}", Config::default().runtime.display()); + Ok(0) } - Some(val) => Err(Error::generic(format!( - "'{}' isn't a valid value to print", - val - ))), - None => Err(Error::generic( + Some(val) => { + Err(Error::from(format!("'{}' isn't a valid value to print", val))) + } + None => Err(Error::from( "You must specify a type of value to print".to_string(), )), } diff --git a/inko/src/command/run.rs b/inko/src/command/run.rs index ae272d0a3..4edd1d8a0 100644 --- a/inko/src/command/run.rs +++ b/inko/src/command/run.rs @@ -77,7 +77,7 @@ pub(crate) fn run(arguments: &[String]) -> Result { if !build_dir.is_dir() { create_dir(&build_dir).map_err(|err| { - Error::generic(format!( + Error::from(format!( "Failed to create {}: {}", build_dir.display(), err @@ -100,7 +100,7 @@ pub(crate) fn run(arguments: &[String]) -> Result { .spawn() .and_then(|mut child| child.wait()) .map_err(|err| { - Error::generic(format!( + Error::from(format!( "Failed to run the executable: {}", err )) @@ -116,6 +116,6 @@ pub(crate) fn run(arguments: &[String]) -> Result { status } Err(CompileError::Invalid) => Ok(1), - Err(CompileError::Internal(msg)) => Err(Error::generic(msg)), + Err(CompileError::Internal(msg)) => Err(Error::from(msg)), } } diff --git a/inko/src/command/runtime.rs b/inko/src/command/runtime.rs new file mode 100644 index 000000000..7a7793cf8 --- /dev/null +++ b/inko/src/command/runtime.rs @@ -0,0 +1,64 @@ +mod add; +mod list; +mod remove; + +use crate::error::Error; +use crate::options::print_usage; +use compiler::config::local_runtimes_directory; +use getopts::{Options, ParsingStyle}; +use std::fs::create_dir_all; + +const USAGE: &str = "inko runtime [OPTIONS] [COMMAND] + +Manage runtimes for the available targets. + +Commands: + + add Install a new runtime + remove Remove an installed runtime + list List the available and installed runtimes + +Examples: + + inko runtime add arm64-linux-gnu + inko runtime remove arm64-linux-gnu"; + +pub(crate) fn run(arguments: &[String]) -> Result { + let mut options = Options::new(); + + options.parsing_style(ParsingStyle::StopAtFirstFree); + options.optflag("h", "help", "Show this help message"); + + let matches = options.parse(arguments)?; + + if matches.opt_present("h") { + print_usage(&options, USAGE); + return Ok(0); + } + + // Instead of each command having to check if the runtimes directory exists, + // we ensure it does here. + let runtimes = local_runtimes_directory().ok_or_else(|| { + Error::from("failed to determine the runtimes directory".to_string()) + })?; + + if let Err(e) = create_dir_all(&runtimes) { + return Err(Error::from(format!( + "failed to create the runtimes directory: {}", + e + ))); + } + + match matches.free.first().map(|s| s.as_str()) { + Some("add") => add::run(runtimes, &matches.free[1..]), + Some("remove") => remove::run(runtimes, &matches.free[1..]), + Some("list") => list::run(runtimes, &matches.free[1..]), + Some(cmd) => { + Err(Error::from(format!("The command {:?} is invalid", cmd))) + } + None => { + print_usage(&options, USAGE); + Ok(0) + } + } +} diff --git a/inko/src/command/runtime/add.rs b/inko/src/command/runtime/add.rs new file mode 100644 index 000000000..e609a538c --- /dev/null +++ b/inko/src/command/runtime/add.rs @@ -0,0 +1,180 @@ +use crate::error::Error; +use crate::http; +use crate::options::print_usage; +use compiler::target::Target; +use flate2::read::GzDecoder; +use getopts::{Options, ParsingStyle}; +use std::env::temp_dir; +use std::fs::{remove_dir_all, remove_file, File}; +use std::io::{stdout, IsTerminal as _}; +use std::io::{Read as _, Write as _}; +use std::path::{Path, PathBuf}; +use tar::Archive; + +/// The base URL from which to download the runtime files. +pub(crate) const URL: &str = "https://releases.inko-lang.org"; + +const USAGE: &str = "inko runtime add [OPTIONS] TARGET + +Add a new runtime for a given target. + +Examples: + + inko runtime add arm64-linux-gnu"; + +struct ProgressBar { + current: usize, + total: usize, + last_percentage: usize, +} + +impl ProgressBar { + fn new(total: usize) -> ProgressBar { + ProgressBar { current: 0, total, last_percentage: 0 } + } + + fn add(&mut self, amount: usize) { + self.current += amount; + + let percent = + (((self.current as f64) / (self.total as f64)) * 100.0) as usize; + + if percent != self.last_percentage { + let done_mb = (self.current as f64 / 1024.0 / 1024.0) as usize; + let total_mb = (self.total as f64 / 1024.0 / 1024.0) as usize; + + self.last_percentage = percent; + print!("\r {} MiB / {} MiB ({}%)", done_mb, total_mb, percent); + + let _ = stdout().flush(); + } + } +} + +impl Drop for ProgressBar { + fn drop(&mut self) { + // This ensures that we always produce a new line after the progress + // line, even in the event of an error, ensuring future output isn't + // placed on the same line. + if self.last_percentage > 0 { + println!(); + } + } +} + +fn download(target: &Target) -> Result { + let archive_name = format!("{}.tar.gz", target); + let url = format!( + "{}/runtimes/{}/{}", + URL, + env!("CARGO_PKG_VERSION"), + archive_name, + ); + + let response = http::get(&url)?; + let total = response + .header("Content-Length") + .and_then(|v| v.parse::().ok()) + .unwrap_or(0); + + // We don't decompress here right away as that prevents us from reporting + // progress correctly (due to the total read size being different from the + // Content-Length value). + let mut reader = response.into_reader(); + let path = temp_dir().join(archive_name); + let mut file = File::create(&path).map_err(|e| { + Error::from(format!("failed to open {}: {}", path.display(), e)) + })?; + + let mut run = true; + let mut progress = ProgressBar::new(total); + let is_term = stdout().is_terminal(); + + while run { + let mut buff = [0_u8; 8096]; + let result = reader + .read(&mut buff) + .and_then(|len| file.write_all(&buff[0..len]).map(|_| len)); + + let read = match result { + Ok(0) => { + run = false; + 0 + } + Ok(n) => n, + Err(err) => { + return Err(Error::from(format!( + "failed to download the runtime: {}", + err + ))); + } + }; + + if is_term { + progress.add(read) + } + } + + Ok(path) +} + +fn unpack(path: &Path, into: &Path) -> Result<(), String> { + let archive = File::open(path).map_err(|e| e.to_string())?; + let res = Archive::new(GzDecoder::new(archive)) + .entries() + .and_then(|entries| { + for entry in entries { + entry.and_then(|mut entry| entry.unpack_in(into))?; + } + + Ok(()) + }) + .map_err(|e| format!("failed to unpack the archive: {}", e)) + .and_then(|_| remove_file(path).map_err(|e| e.to_string())) + .map(|_| ()); + + // This ensures that in the event of an error, we don't leave behind a + // partially decompressed archive in the runtimes directory. + if res.is_err() && into.is_dir() { + let _ = remove_dir_all(into); + } + + res +} + +pub(crate) fn run( + runtimes: PathBuf, + arguments: &[String], +) -> Result { + let mut options = Options::new(); + + options.parsing_style(ParsingStyle::StopAtFirstFree); + options.optflag("h", "help", "Show this help message"); + + let matches = options.parse(arguments)?; + + if matches.opt_present("h") { + print_usage(&options, USAGE); + return Ok(0); + } + + let target = + matches.free.first().and_then(|v| Target::parse(v)).ok_or_else( + || Error::from("a valid target triple is required".to_string()), + )?; + + if runtimes.join(target.to_string()).is_dir() { + return Err(Error::from(format!( + "the runtime for the target '{}' is already installed", + target + ))); + } + + println!("Downloading runtime for target '{}'...", target); + + let tmp_path = download(&target)?; + + unpack(&tmp_path, &runtimes).map(|_| 0).map_err(|e| { + Error::from(format!("failed to decompress the runtime: {}", e)) + }) +} diff --git a/inko/src/command/runtime/list.rs b/inko/src/command/runtime/list.rs new file mode 100644 index 000000000..5dc7eef7c --- /dev/null +++ b/inko/src/command/runtime/list.rs @@ -0,0 +1,51 @@ +use crate::error::Error; +use crate::options::print_usage; +use compiler::target::Target; +use getopts::{Options, ParsingStyle}; +use std::io::{stdout, IsTerminal as _}; +use std::path::PathBuf; + +const USAGE: &str = "inko runtime list [OPTIONS] + +List the targets for which a runtime is available, and highlights those for +which a runtime is installed. + +Examples: + + inko runtime list"; + +pub(crate) fn run( + runtimes: PathBuf, + arguments: &[String], +) -> Result { + let mut options = Options::new(); + + options.parsing_style(ParsingStyle::StopAtFirstFree); + options.optflag("h", "help", "Show this help message"); + + let matches = options.parse(arguments)?; + let is_term = stdout().is_terminal(); + + if matches.opt_present("h") { + print_usage(&options, USAGE); + return Ok(0); + } + + for target in Target::runtimes() { + let dir = runtimes.join(target.to_string()); + + if dir.is_dir() { + let line = format!("{} (installed)", target); + + if is_term { + println!("\x1b[1m{}\x1b[0m", line); + } else { + println!("{}", line); + } + } else { + println!("{}", target); + } + } + + Ok(0) +} diff --git a/inko/src/command/runtime/remove.rs b/inko/src/command/runtime/remove.rs new file mode 100644 index 000000000..16384d863 --- /dev/null +++ b/inko/src/command/runtime/remove.rs @@ -0,0 +1,51 @@ +use crate::error::Error; +use crate::options::print_usage; +use compiler::target::Target; +use getopts::{Options, ParsingStyle}; +use std::fs::remove_dir_all; +use std::path::PathBuf; + +const USAGE: &str = "inko runtime remove [OPTIONS] TARGET + +Remove an existing runtime for a given target. + +Examples: + + inko runtime remove arm64-linux-gnu"; + +pub(crate) fn run( + runtimes: PathBuf, + arguments: &[String], +) -> Result { + let mut options = Options::new(); + + options.parsing_style(ParsingStyle::StopAtFirstFree); + options.optflag("h", "help", "Show this help message"); + + let matches = options.parse(arguments)?; + + if matches.opt_present("h") { + print_usage(&options, USAGE); + return Ok(0); + } + + let target = + matches.free.first().and_then(|v| Target::parse(v)).ok_or_else( + || Error::from("a valid target triple is required".to_string()), + )?; + + let dir = runtimes.join(target.to_string()); + + if !dir.is_dir() { + return Err(Error::from(format!( + "no runtime for the target '{}' is installed", + target + ))); + } + + remove_dir_all(&dir) + .map_err(|e| { + Error::from(format!("failed to remove {}: {}", dir.display(), e)) + }) + .map(|_| 0) +} diff --git a/inko/src/command/targets.rs b/inko/src/command/targets.rs new file mode 100644 index 000000000..f3ad0ca2d --- /dev/null +++ b/inko/src/command/targets.rs @@ -0,0 +1,44 @@ +use crate::error::Error; +use crate::options::print_usage; +use compiler::target::Target; +use getopts::{Options, ParsingStyle}; +use std::io::{stdout, IsTerminal as _}; + +const USAGE: &str = "inko targets [OPTIONS] + +List the supported target triples. + +Examples: + + inko targets"; + +pub(crate) fn run(arguments: &[String]) -> Result { + let mut options = Options::new(); + + options.parsing_style(ParsingStyle::StopAtFirstFree); + options.optflag("h", "help", "Show this help message"); + + let matches = options.parse(arguments)?; + let is_term = stdout().is_terminal(); + + if matches.opt_present("h") { + print_usage(&options, USAGE); + return Ok(0); + } + + for target in Target::supported() { + if target.is_native() { + let line = format!("{} (native)", target); + + if is_term { + println!("\x1b[1m{}\x1b[0m", line); + } else { + println!("{}", line); + } + } else { + println!("{}", target); + } + } + + Ok(0) +} diff --git a/inko/src/command/test.rs b/inko/src/command/test.rs index 5fa0df5ca..3a1e02bd6 100644 --- a/inko/src/command/test.rs +++ b/inko/src/command/test.rs @@ -36,7 +36,7 @@ pub(crate) fn run(arguments: &[String]) -> Result { let input = config.main_test_module(); if !config.tests.is_dir() { - return Err(Error::generic(format!( + return Err(Error::from(format!( "The tests directory {:?} doesn't exist", config.tests ))); @@ -46,7 +46,7 @@ pub(crate) fn run(arguments: &[String]) -> Result { config.output = Output::File("inko-tests".to_string()); let tests = test_module_names(&config.tests).map_err(|err| { - Error::generic(format!("Failed to find test modules: {}", err)) + Error::from(format!("Failed to find test modules: {}", err)) })?; let mut compiler = Compiler::new(config); @@ -56,7 +56,7 @@ pub(crate) fn run(arguments: &[String]) -> Result { compiler.create_build_directory()?; write(&input, generate_main_test_module(tests)).map_err(|err| { - Error::generic(format!("Failed to write {}: {}", input.display(), err)) + Error::from(format!("Failed to write {}: {}", input.display(), err)) })?; let result = compiler.build(Some(input)); @@ -69,11 +69,11 @@ pub(crate) fn run(arguments: &[String]) -> Result { .spawn() .and_then(|mut child| child.wait()) .map_err(|err| { - Error::generic(format!("Failed to run the tests: {}", err)) + Error::from(format!("Failed to run the tests: {}", err)) }) .map(|status| status.code().unwrap_or(0)), Err(CompileError::Invalid) => Ok(1), - Err(CompileError::Internal(msg)) => Err(Error::generic(msg)), + Err(CompileError::Internal(msg)) => Err(Error::from(msg)), } } diff --git a/inko/src/error.rs b/inko/src/error.rs index 1fc44e78c..b4592fa7b 100644 --- a/inko/src/error.rs +++ b/inko/src/error.rs @@ -11,26 +11,20 @@ pub(crate) struct Error { pub(crate) message: Option, } -impl Error { - pub(crate) fn generic(message: String) -> Self { - Error { status: 1, message: Some(message) } - } -} - impl From for Error { fn from(fail: Fail) -> Self { - Self::generic(fail.to_string()) + Self::from(fail.to_string()) } } -impl From for Error { - fn from(error: io::Error) -> Self { - Self::generic(error.to_string()) +impl From for Error { + fn from(message: String) -> Self { + Error { status: 1, message: Some(message) } } } -impl From for Error { - fn from(message: String) -> Self { - Self::generic(message) +impl From for Error { + fn from(error: io::Error) -> Self { + Error { status: 1, message: Some(error.to_string()) } } } diff --git a/inko/src/http.rs b/inko/src/http.rs new file mode 100644 index 000000000..84c64f019 --- /dev/null +++ b/inko/src/http.rs @@ -0,0 +1,30 @@ +use crate::error::Error; +use std::time::Duration; +use ureq::{self, Agent, Error as HttpError, Response}; + +const TIMEOUT: u64 = 10; + +pub(crate) fn get(url: &str) -> Result { + let agent = agent(); + + match agent.get(url).call() { + Ok(response) => Ok(response), + Err(HttpError::Status(code, response)) => Err(Error::from(format!( + "GET {} failed: HTTP {} {}", + url, + code, + response.status_text() + ))), + Err(HttpError::Transport(err)) => { + Err(Error::from(format!("GET {} failed: {}", url, err))) + } + } +} + +fn agent() -> Agent { + ureq::builder() + .timeout_connect(Duration::from_secs(TIMEOUT)) + .timeout_read(Duration::from_secs(TIMEOUT)) + .user_agent(&format!("inko {}", env!("CARGO_PKG_VERSION"))) + .build() +} diff --git a/inko/src/main.rs b/inko/src/main.rs index beeb199f2..27d90d602 100644 --- a/inko/src/main.rs +++ b/inko/src/main.rs @@ -1,9 +1,11 @@ mod command; mod error; +mod http; mod options; mod pkg; use crate::command::main; +use std::io::{stdout, IsTerminal as _}; use std::process::exit; fn main() { @@ -11,7 +13,11 @@ fn main() { Ok(status) => exit(status), Err(err) => { if let Some(message) = err.message { - eprintln!("{}", message); + if stdout().is_terminal() { + eprintln!("\x1b[31;1merror:\x1b[0m {}", message); + } else { + eprintln!("error: {}", message); + } } exit(err.status); diff --git a/inko/src/pkg/util.rs b/inko/src/pkg/util.rs index 6851498ef..629281a64 100644 --- a/inko/src/pkg/util.rs +++ b/inko/src/pkg/util.rs @@ -1,22 +1,10 @@ -use std::env; +use compiler::config; use std::fs::{copy, create_dir_all, read_dir}; use std::path::{Path, PathBuf}; -fn home_dir() -> Option { - env::var_os("HOME").filter(|v| !v.is_empty()).map(PathBuf::from) -} - pub(crate) fn data_dir() -> Result { - let base = if cfg!(target_os = "macos") { - home_dir().map(|h| h.join("Library").join("Application Support")) - } else { - env::var_os("XDG_DATA_HOME") - .filter(|v| !v.is_empty()) - .map(PathBuf::from) - .or_else(|| home_dir().map(|h| h.join(".local").join("share"))) - }; - - base.map(|p| p.join("inko").join("packages")) + config::data_directory() + .map(|p| p.join("packages")) .ok_or_else(|| "No data directory could be determined".to_string()) } diff --git a/rt/src/lib.rs b/rt/src/lib.rs index cdc264fb0..78a2d8256 100644 --- a/rt/src/lib.rs +++ b/rt/src/lib.rs @@ -3,6 +3,9 @@ #![cfg_attr(feature = "cargo-clippy", allow(clippy::missing_safety_doc))] #![cfg_attr(feature = "cargo-clippy", allow(clippy::too_many_arguments))] +#[cfg(feature = "unwinding")] +extern crate unwinding; + pub mod macros; pub mod arc_without_weak; diff --git a/scripts/runtimes.sh b/scripts/runtimes.sh new file mode 100755 index 000000000..23b5f137c --- /dev/null +++ b/scripts/runtimes.sh @@ -0,0 +1,85 @@ +#!/usr/bin/env bash + +set -e + +# We use a nightly version as close as possible to the minimum Rust version that +# we require. This version is obtained by going through the releases at +# https://releases.rs/ and picking a nightly close to the branch date of the +# desired release. +# +# This is needed so we can use the unwinding crate while still having a somewhat +# stable version of Rust, instead of relying on the latest nightly version that +# may randomly change. + +# The Inko version we're building the runtimes for. +VERSION="${1}" + +# The directory to place the runtimes in. +DIR="tmp/runtimes/${VERSION}" + +# The S3 bucket to store the runtime files in. +S3_BUCKET="releases.inko-lang.org" + +function rustup_lib { + local home + local toolchain + home="$(rustup show home)" + toolchain="$(rustup show active-toolchain | awk '{print $1}')" + + echo "${home}/toolchains/${toolchain}/lib/rustlib/${1}/lib/" +} + +function build { + local rust_target + local inko_target + local out + local target_dir + rust_target="${1}" + inko_target="${2}" + out="${DIR}/${inko_target}.tar.gz" + target_dir="${DIR}/${inko_target}" + + if [[ -f "${out}" ]] + then + return 0 + fi + + rustup target add "${rust_target}" + cargo build -p rt --release --target="${rust_target}" + mkdir -p "${target_dir}" + cp "target/${rust_target}/release/libinko.a" "${target_dir}" + + if [[ "${rust_target}" == *-musl ]] + then + cp "$(rustup_lib "${rust_target}")/self-contained/libunwind.a" \ + "${target_dir}" + fi + + tar --directory "${DIR}" --create --gzip --file "${out}" "${inko_target}" + rm -rf "${target_dir}" +} + +if [[ "${VERSION}" = "" ]] +then + echo 'You must specify the Inko version to build the runtimes for' + exit 1 +fi + +mkdir -p "${DIR}" + +# FreeBSD +build "x86_64-unknown-freebsd" "amd64-freebsd-native" foo bar + +# Linux +build "x86_64-unknown-linux-gnu" "amd64-linux-gnu" +build "x86_64-unknown-linux-musl" "amd64-linux-musl" +build "aarch64-unknown-linux-gnu" "arm64-linux-gnu" +build "aarch64-unknown-linux-musl" "arm64-linux-musl" + +# macOS +build "x86_64-apple-darwin" "amd64-mac-native" +build "aarch64-apple-darwin" "arm64-mac-native" + +# Upload the results to the S3 bucket. +aws s3 sync --no-progress --acl=public-read --cache-control max-age=86400 \ + "${DIR}" "s3://${S3_BUCKET}/runtimes/${VERSION}" diff --git a/std/src/std/fs/path.inko b/std/src/std/fs/path.inko index 1b23f284f..ab24902c1 100644 --- a/std/src/std/fs/path.inko +++ b/std/src/std/fs/path.inko @@ -211,6 +211,9 @@ class pub Path { # Returns the creation time of `self`. # + # The target platform may not supported getting the creation time, in which + # case an `Error` is returned. musl targets are an example of such a platform. + # # # Examples # # Obtaining the creation time of a `Path`: @@ -233,6 +236,9 @@ class pub Path { # Returns the modification time of `self`. # + # The target platform may not supported getting the creation time, in which + # case an `Error` is returned. + # # # Examples # # Obtaining the modification time of a `Path`: @@ -255,6 +261,9 @@ class pub Path { # Returns the access time of `self`. # + # The target platform may not supported getting the creation time, in which + # case an `Error` is returned. + # # # Examples # # Obtaining the access time of a `Path`: diff --git a/std/test/helpers.inko b/std/test/helpers.inko index 7c75e93e1..72fe7c2fa 100644 --- a/std/test/helpers.inko +++ b/std/test/helpers.inko @@ -5,6 +5,9 @@ import std.fs.path.Path import std.hash.(Hash, Hasher) import std.hash.siphash.SipHasher13 +import std.fmt.(fmt) +import std.stdio.STDOUT + fn pub hash[T: Hash](value: ref T) -> Int { let hasher = SipHasher13.default @@ -36,7 +39,7 @@ fn pub compiler_path -> Path { let debug = target.join('debug').join('inko') let release = target.join('release').join('inko') - match (debug.created_at, release.created_at) { + match (debug.modified_at, release.modified_at) { case (Ok(deb), Ok(rel)) -> if deb >= rel { debug } else { release } case (Ok(_), Error(_)) -> debug case (Error(_), Ok(_)) -> release