diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..35049cb --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6500555 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,89 @@ +name: CI + +on: + push: + branches: + - main + pull_request: + branches: + - main + schedule: + - cron: 00 4 * * * + +env: + CARGO_TERM_COLOR: always + +jobs: + llvm: + uses: ./.github/workflows/llvm.yml + + lint-stable: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@master + with: + toolchain: stable + components: clippy, rust-src + + - name: Run clippy + run: cargo clippy --all-targets --workspace -- --deny warnings + + lint-nightly: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly + components: rustfmt, rust-src + + - name: Check formatting + run: cargo fmt --all -- --check + + build-container-image: + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + include: + - target: aarch64-unknown-linux-gnu + type: cross + - target: aarch64-unknown-linux-musl + type: cross + - target: riscv64gc-unknown-linux-gnu + type: cross + - target: x86_64-unknown-linux-gnu + type: native + - target: x86_64-unknown-linux-musl + type: native + name: container ${{ matrix.type }} ${{ matrix.target }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + + - name: Log in to GitHub Container Registry + run: | + set -euxo pipefail + echo "${{ secrets.GITHUB_TOKEN }}" | \ + docker login ghcr.io -u ${{ github.actor }} --password-stdin + + - name: Build container image + if: github.ref == 'refs/heads/main' + run: | + cargo xtask build-container-image \ + --tag ghcr.io/${{ github.repository }}/${{ matrix.type }}-${{ matrix.target }}:latest \ + --target ${{ matrix.target }} \ + --push + + - name: Build container image + if: github.ref != 'refs/heads/main' + run: | + cargo xtask build-container-image \ + --tag ghcr.io/${{ github.repository }}/${{ matrix.type }}-${{ matrix.target }}:${{ github.head_ref }} \ + --target ${{ matrix.target }} \ + --push diff --git a/.gitignore b/.gitignore index d01bd1a..efe3eb1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,4 +18,8 @@ Cargo.lock # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. -#.idea/ \ No newline at end of file +#.idea/ + +# Added by cargo + +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..2606e7f --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,16 @@ +[workspace] +members = [ + "cross-llvm", + "xtask", +] + +[workspace.dependencies] +anyhow = "1.0.89" +chrono = "0.4" +clap = { version = "4.5", features = ["derive"] } +target-lexicon = "0.12" +thiserror = { version = "1.0.64" } +uuid = { version = "1.10", features = ["v4"] } +which = "6.0" + +cross-llvm = { path = "cross-llvm" } diff --git a/containers/Dockerfile.cross-aarch64-unknown-linux-gnu b/containers/Dockerfile.cross-aarch64-unknown-linux-gnu new file mode 100644 index 0000000..0d01508 --- /dev/null +++ b/containers/Dockerfile.cross-aarch64-unknown-linux-gnu @@ -0,0 +1,30 @@ +FROM docker.io/debian:bookworm + +ENV PATH="/root/.cargo/bin:/usr/lib/llvm-15/bin:${PATH}" + +# Even though we are using clang as a C compiler, we need the libgcc_s and +# libstdc++. +RUN dpkg --add-architecture arm64 \ + && apt update \ + && apt install -y \ + build-essential \ + clang-15 \ + cmake \ + curl \ + gcc-aarch64-linux-gnu \ + g++-aarch64-linux-gnu \ + libc6-dev-arm64-cross \ + libzstd-dev \ + libzstd-dev:arm64 \ + lld-15 \ + ninja-build \ + qemu-user \ + zlib1g-dev \ + zlib1g-dev:arm64 \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + && rustup toolchain install stable --component rust-src \ + && rustup toolchain install nightly --component rust-src \ + && rustup target add aarch64-unknown-linux-gnu \ + && rustup +nightly target add aarch64-unknown-linux-gnu \ + && cargo install btfdump \ + && rm -rf /var/lib/apt/lists/* diff --git a/containers/Dockerfile.cross-aarch64-unknown-linux-musl b/containers/Dockerfile.cross-aarch64-unknown-linux-musl new file mode 100644 index 0000000..923485e --- /dev/null +++ b/containers/Dockerfile.cross-aarch64-unknown-linux-musl @@ -0,0 +1,76 @@ +FROM docker.io/gentoo/stage3:musl-llvm + +ENV PATH="/root/.cargo/bin:/usr/lib/llvm/18/bin:${PATH}" +# Install only aarch64 user-space wrapper when installing app-emulation/qemu. +ENV QEMU_USER_TARGETS="aarch64" +# Enable static libraries for installed packages (zstd, zlib etc.). +ENV USE="static-libs" + +# Use clang-musl-overlay patches[0], needed for QEMU to build successfully. +# +# Install llvm-libgcc, which is a drop-in replacement for libgcc_s runtime +# library. It's needed by Rust binaries provided by rustup[1][2]. It's provided +# in vadorovsky's private overlay. +# +# Create a cross sysroot using crossdev[3], which also creates wrappers for: +# - clang, which can be used for compiling C/C++ projects without doing the +# whole dance with `--target` and `--sysroot` arguments. +# - emerge, which let you install packages in the cross sysroot. +# +# Unpack the stage3 tarball into that sysroot to avoiding compilation of the +# whole base system from scratch. Otherwise, +# `emerge-aarch64-unknown-linux-musl @system` would take an eternity to run on +# free GitHub runners. +# +# Patch the clang config to use libc++ and libunwind, before the necessary fix +# in crossdev gets merged[4]. +# +# [0] https://github.com/clang-musl-overlay/gentoo-patchset +# [1] https://github.com/rust-lang/rust/issues/119504 +# [2] https://github.com/rust-lang/rustup/issues/2213#issuecomment-1888615413 +# [3] https://wiki.gentoo.org/wiki/Crossdev +# [4] https://github.com/gentoo/crossdev/pull/23 +RUN emerge --sync --quiet \ + && emerge \ + app-eselect/eselect-repository \ + dev-vcs/git \ + sys-devel/crossdev \ + && eselect repository add vadorovsky git \ + https://gitlab.com/vadorovsky/overlay \ + && git clone --depth 1 \ + https://github.com/clang-musl-overlay/gentoo-patchset \ + /etc/portage/patches \ + && emerge --sync --quiet \ + && emerge \ + app-emulation/qemu \ + sys-libs/llvm-libgcc \ + && eselect repository create crossdev \ + && crossdev --llvm --target aarch64-unknown-linux-musl \ + && curl -L "https://ftp-osl.osuosl.org/pub/gentoo/releases/arm64/autobuilds/current-stage3-arm64-musl-llvm/$(\ + curl -L "https://ftp-osl.osuosl.org/pub/gentoo/releases/arm64/autobuilds/current-stage3-arm64-musl-llvm/latest-stage3-arm64-musl-llvm.txt" | \ + grep tar.xz | cut -d ' ' -f 1)" | \ + tar -xJpf - -C /usr/aarch64-unknown-linux-musl --exclude=dev --skip-old-files \ + && ln -s \ + /etc/portage/repos.conf \ + /usr/aarch64-unknown-linux-musl/etc/portage/repos.conf \ + && PORTAGE_CONFIGROOT=/usr/aarch64-unknown-linux-musl eselect profile set \ + default/linux/arm64/23.0/musl/llvm \ + && sed -i -e "s/--unwindlib=none/--unwindlib=libunwind/" \ + /etc/clang/cross/aarch64-unknown-linux-musl.cfg \ + && echo "--stdlib=libc++" >> \ + /etc/clang/cross/aarch64-unknown-linux-musl.cfg \ + && aarch64-unknown-linux-musl-emerge --sync --quiet \ + && aarch64-unknown-linux-musl-emerge \ + app-arch/zstd \ + sys-libs/llvm-libgcc \ + sys-libs/zlib \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + && rustup toolchain install stable --component rust-src \ + && rustup toolchain install nightly --component rust-src \ + && rustup target add aarch64-unknown-linux-musl \ + && rustup +nightly target add aarch64-unknown-linux-musl \ + && cargo install btfdump \ + && rm -rf \ + /var/cache/binpkgs/* \ + /var/cache/distfiles/* \ + /var/tmp/portage/* diff --git a/containers/Dockerfile.cross-riscv64gc-unknown-linux-gnu b/containers/Dockerfile.cross-riscv64gc-unknown-linux-gnu new file mode 100644 index 0000000..aa83505 --- /dev/null +++ b/containers/Dockerfile.cross-riscv64gc-unknown-linux-gnu @@ -0,0 +1,30 @@ +FROM docker.io/debian:trixie + +ENV PATH="/root/.cargo/bin:/usr/lib/llvm-15/bin:${PATH}" + +# Even though we are using clang as a C compiler, we need the libgcc_s and +# libstdc++. +RUN dpkg --add-architecture riscv64 \ + && apt update \ + && apt install -y \ + build-essential \ + clang-15 \ + cmake \ + curl \ + gcc-riscv64-linux-gnu \ + g++-riscv64-linux-gnu \ + libc6-dev-riscv64-cross \ + libzstd-dev \ + libzstd-dev:riscv64 \ + lld-15 \ + ninja-build \ + qemu-user \ + zlib1g-dev \ + zlib1g-dev:riscv64 \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + && rustup toolchain install stable --component rust-src \ + && rustup toolchain install nightly --component rust-src \ + && rustup target add aarch64-unknown-linux-gnu \ + && rustup +nightly target add aarch64-unknown-linux-gnu \ + && cargo install btfdump \ + && rm -rf /var/lib/apt/lists/* diff --git a/containers/Dockerfile.native-x86_64-unknown-linux-gnu b/containers/Dockerfile.native-x86_64-unknown-linux-gnu new file mode 100644 index 0000000..7c0ca71 --- /dev/null +++ b/containers/Dockerfile.native-x86_64-unknown-linux-gnu @@ -0,0 +1,19 @@ +FROM docker.io/debian:bookworm + +ENV PATH="/root/.cargo/bin:/usr/lib/llvm-15/bin:${PATH}" + +RUN apt update \ + && apt install -y \ + build-essential \ + clang-15 \ + cmake \ + curl \ + libzstd-dev \ + lld-15 \ + ninja-build \ + zlib1g-dev \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + && rustup toolchain install stable --component rust-src \ + && rustup toolchain install nightly --component rust-src \ + && cargo install btfdump \ + && rm -rf /var/lib/apt/lists/* diff --git a/containers/Dockerfile.native-x86_64-unknown-linux-musl b/containers/Dockerfile.native-x86_64-unknown-linux-musl new file mode 100644 index 0000000..d50e383 --- /dev/null +++ b/containers/Dockerfile.native-x86_64-unknown-linux-musl @@ -0,0 +1,28 @@ +FROM docker.io/gentoo/stage3:musl-llvm + +ENV PATH="/root/.cargo/bin:/usr/lib/llvm/18/bin:${PATH}" +# Enable static libraries for installed packages (zstd, zlib etc.). +ENV USE="static-libs" + +# Install llvm-libgcc, which is a drop-in replacement for libgcc_s runtime +# library. It's needed by Rust binaries provided by rustup[0][1]. +# +# [0] https://github.com/rust-lang/rust/issues/119504 +# [1] https://github.com/rust-lang/rustup/issues/2213#issuecomment-1888615413 +RUN emerge --sync --quiet \ + && emerge \ + app-arch/zstd \ + app-eselect/eselect-repository \ + dev-vcs/git \ + sys-libs/zlib \ + && eselect repository add vadorovsky git https://gitlab.com/vadorovsky/overlay \ + && emerge --sync --quiet \ + && emerge sys-libs/llvm-libgcc \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ + && rustup toolchain install stable --component rust-src \ + && rustup toolchain install nightly --component rust-src \ + && cargo install btfdump \ + && rm -rf \ + /var/cache/binpkgs/* \ + /var/cache/distfiles/* \ + /var/tmp/portage/* diff --git a/cross-llvm/Cargo.toml b/cross-llvm/Cargo.toml new file mode 100644 index 0000000..c0ddb37 --- /dev/null +++ b/cross-llvm/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "cross-llvm" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +clap = { workspace = true } +target-lexicon = { workspace = true } +thiserror = { workspace = true } +which = { workspace = true } + +[[bin]] +name = "cross-llvm" diff --git a/cross-llvm/src/bin/cross-llvm.rs b/cross-llvm/src/bin/cross-llvm.rs new file mode 100644 index 0000000..6512c2d --- /dev/null +++ b/cross-llvm/src/bin/cross-llvm.rs @@ -0,0 +1,26 @@ +use clap::{Parser, Subcommand}; + +use cross_llvm::run::{run, Run}; + +/// Containerized (cross, but not only) LLVM toolchains. +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Subcommands + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Run a custom command. + Run(Run), +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::Run(args) => run(args), + } +} diff --git a/cross-llvm/src/containers.rs b/cross-llvm/src/containers.rs new file mode 100644 index 0000000..c752da2 --- /dev/null +++ b/cross-llvm/src/containers.rs @@ -0,0 +1,31 @@ +use clap::ValueEnum; +use which::which; + +use crate::errors::CrossLlvmError; + +#[derive(Clone, ValueEnum)] +pub enum ContainerEngine { + Docker, + Podman, +} + +impl ToString for ContainerEngine { + fn to_string(&self) -> String { + match self { + Self::Docker => "docker".to_owned(), + Self::Podman => "podman".to_owned(), + } + } +} + +impl ContainerEngine { + pub fn autodetect() -> Result { + if which("docker").is_ok() { + Ok(Self::Docker) + } else if which("podman").is_ok() { + Ok(Self::Podman) + } else { + Err(CrossLlvmError::ContainerEngineNotFound) + } + } +} diff --git a/cross-llvm/src/errors.rs b/cross-llvm/src/errors.rs new file mode 100644 index 0000000..d9fc4d7 --- /dev/null +++ b/cross-llvm/src/errors.rs @@ -0,0 +1,7 @@ +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum CrossLlvmError { + #[error("no supported container engine (docker, podman) was found")] + ContainerEngineNotFound, +} diff --git a/cross-llvm/src/lib.rs b/cross-llvm/src/lib.rs new file mode 100644 index 0000000..84cde9b --- /dev/null +++ b/cross-llvm/src/lib.rs @@ -0,0 +1,4 @@ +pub mod containers; +pub mod errors; +pub mod run; +pub mod target; diff --git a/cross-llvm/src/run.rs b/cross-llvm/src/run.rs new file mode 100644 index 0000000..5075cf9 --- /dev/null +++ b/cross-llvm/src/run.rs @@ -0,0 +1,75 @@ +use std::{ + env, + ffi::{OsStr, OsString}, + process::{Command, Stdio}, +}; + +use clap::Parser; +use target_lexicon::Triple; + +use crate::{ + containers::ContainerEngine, + target::{SupportedTriple, TripleExt}, +}; + +#[derive(Parser)] +pub struct Run { + /// Container engine (if not provided, is going to be autodetected) + #[arg(long)] + container_engine: Option, + + /// Container image to use. + #[arg(long)] + container_image: Option, + + /// The command to run inside the container. + #[arg(trailing_var_arg = true)] + cmd: Vec, + + /// Target triple (optional) + #[arg(long)] + target: Option, +} + +pub fn run(args: Run) -> anyhow::Result<()> { + let Run { + container_engine, + container_image, + cmd, + target, + } = args; + + let triple: Triple = match target { + Some(target) => target.into(), + None => target_lexicon::HOST, + }; + + let container_engine = container_engine.unwrap_or(ContainerEngine::autodetect()?); + let container_image = container_image.unwrap_or(triple.default_container_tag()); + + let mut bind_mount = env::current_dir()?.into_os_string(); + bind_mount.push(":/src"); + + let mut container = Command::new(container_engine.to_string()); + container + .args([ + OsStr::new("run"), + OsStr::new("--rm"), + OsStr::new("-it"), + OsStr::new("-v"), + &bind_mount, + OsStr::new("-w"), + OsStr::new("/src"), + &container_image, + ]) + .args(cmd) + .stdin(Stdio::inherit()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + println!("{container:?}"); + + let mut child = container.spawn()?; + child.wait()?; + + Ok(()) +} diff --git a/cross-llvm/src/target.rs b/cross-llvm/src/target.rs new file mode 100644 index 0000000..dc223cf --- /dev/null +++ b/cross-llvm/src/target.rs @@ -0,0 +1,146 @@ +use std::{ffi::OsString, path::Path}; + +use clap::ValueEnum; +use target_lexicon::{ + Aarch64Architecture, Architecture, BinaryFormat, Environment, OperatingSystem, + Riscv64Architecture, Triple, Vendor, +}; + +#[derive(Clone)] +pub enum SupportedTriple { + Aarch64AppleDarwin, + Aarch64UnknownLinuxGnu, + Aarch64UnknownLinuxMusl, + Riscv64GcUnknownLinuxGnu, + X86_64AppleDarwin, + X86_64UnknownLinuxGnu, + X86_64UnknownLinuxMusl, +} + +impl ValueEnum for SupportedTriple { + fn value_variants<'a>() -> &'a [Self] { + &[ + Self::Aarch64AppleDarwin, + Self::Aarch64UnknownLinuxGnu, + Self::Aarch64UnknownLinuxMusl, + Self::Riscv64GcUnknownLinuxGnu, + Self::X86_64AppleDarwin, + Self::X86_64UnknownLinuxGnu, + Self::X86_64UnknownLinuxMusl, + ] + } + + fn to_possible_value(&self) -> Option { + Some(match self { + Self::Aarch64AppleDarwin => clap::builder::PossibleValue::new("aarch64-apple-darwin"), + Self::Aarch64UnknownLinuxGnu => { + clap::builder::PossibleValue::new("aarch64-unknown-linux-gnu") + } + Self::Aarch64UnknownLinuxMusl => { + clap::builder::PossibleValue::new("aarch64-unknown-linux-musl") + } + Self::Riscv64GcUnknownLinuxGnu => { + clap::builder::PossibleValue::new("riscv64gc-unknown-linux-gnu") + } + Self::X86_64AppleDarwin => clap::builder::PossibleValue::new("x86_64-apple-darwin"), + Self::X86_64UnknownLinuxGnu => { + clap::builder::PossibleValue::new("x86_64-unknown-linux-gnu") + } + Self::X86_64UnknownLinuxMusl => { + clap::builder::PossibleValue::new("x86_64-unknown-linux-musl") + } + }) + } +} + +impl From for Triple { + fn from(value: SupportedTriple) -> Self { + match value { + SupportedTriple::Aarch64AppleDarwin => Triple { + architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64), + vendor: Vendor::Apple, + operating_system: OperatingSystem::Darwin, + environment: Environment::Unknown, + binary_format: BinaryFormat::Macho, + }, + SupportedTriple::Aarch64UnknownLinuxGnu => Triple { + architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64), + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Linux, + environment: Environment::Gnu, + binary_format: BinaryFormat::Elf, + }, + SupportedTriple::Aarch64UnknownLinuxMusl => Triple { + architecture: Architecture::Aarch64(Aarch64Architecture::Aarch64), + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Linux, + environment: Environment::Musl, + binary_format: BinaryFormat::Elf, + }, + SupportedTriple::Riscv64GcUnknownLinuxGnu => Triple { + architecture: Architecture::Riscv64(Riscv64Architecture::Riscv64gc), + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Linux, + environment: Environment::Gnu, + binary_format: BinaryFormat::Elf, + }, + SupportedTriple::X86_64AppleDarwin => Triple { + architecture: Architecture::X86_64, + vendor: Vendor::Apple, + operating_system: OperatingSystem::Darwin, + environment: Environment::Unknown, + binary_format: BinaryFormat::Macho, + }, + SupportedTriple::X86_64UnknownLinuxGnu => Triple { + architecture: Architecture::X86_64, + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Linux, + environment: Environment::Gnu, + binary_format: BinaryFormat::Elf, + }, + SupportedTriple::X86_64UnknownLinuxMusl => Triple { + architecture: Architecture::X86_64, + vendor: Vendor::Unknown, + operating_system: OperatingSystem::Linux, + environment: Environment::Musl, + binary_format: BinaryFormat::Elf, + }, + } + } +} + +pub trait TripleExt { + fn default_container_tag(&self) -> OsString; + fn dockerfile(&self) -> Option; + fn is_cross(&self) -> bool; +} + +impl TripleExt for Triple { + fn default_container_tag(&self) -> OsString { + let mut tag = OsString::from("ghcr.io/exein-io/cross-llvm/"); + let prefix = if self.is_cross() { "cross" } else { "native" }; + tag.push(prefix); + tag.push("-"); + tag.push(self.to_string()); + + tag + } + + fn dockerfile(&self) -> Option { + let mut dockerfile = OsString::from("containers/Dockerfile."); + let prefix = if self.is_cross() { "cross" } else { "native" }; + dockerfile.push(prefix); + dockerfile.push("-"); + dockerfile.push(self.to_string()); + + if Path::new(&dockerfile).exists() { + Some(dockerfile) + } else { + None + } + } + + fn is_cross(&self) -> bool { + self.architecture != target_lexicon::HOST.architecture + } +} diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml new file mode 100644 index 0000000..7c36453 --- /dev/null +++ b/xtask/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "xtask" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = { workspace = true } +chrono = { workspace = true } +clap = { workspace = true } +target-lexicon = { workspace = true } +thiserror = { workspace = true } +uuid = { workspace = true } +which = { workspace = true } + +cross-llvm = { workspace = true } diff --git a/xtask/src/containers.rs b/xtask/src/containers.rs new file mode 100644 index 0000000..6598d20 --- /dev/null +++ b/xtask/src/containers.rs @@ -0,0 +1,113 @@ +use std::{ + ffi::{OsStr, OsString}, + process::{Command, Stdio}, +}; + +use clap::Parser; +use target_lexicon::Triple; +use thiserror::Error; + +use cross_llvm::{ + containers::ContainerEngine, + target::{SupportedTriple, TripleExt}, +}; + +#[derive(Debug, Error)] +pub enum ContainerError { + #[error("containerized builds are not supported for target {0}")] + UnsupportedTarget(String), + #[error("failed to build a container image")] + ContainerImageBuild, + #[error("failed to push a container image")] + ContainerImagePush, +} + +#[derive(Parser)] +pub struct BuildContainerImageArgs { + /// Container engine (if not provided, is going to be autodetected) + #[arg(long)] + container_engine: Option, + + /// Do not use existing cached images for the container build. Build from + /// the start with a new set of cached layers. + #[arg(long)] + no_cache: bool, + + /// Push the image after build. + #[arg(long)] + push: bool, + + /// Container image tag. + #[arg(short, long = "tag", name = "tag")] + tags: Vec, + + /// Target triple (optional) + #[arg(long)] + target: Option, +} + +fn push_image(container_engine: &ContainerEngine, tag: &OsStr) -> anyhow::Result<()> { + let mut cmd = Command::new(container_engine.to_string()); + cmd.args([OsStr::new("push"), tag]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + + println!("{cmd:?}"); + if !cmd.status()?.success() { + return Err(ContainerError::ContainerImagePush.into()); + } + + Ok(()) +} + +pub fn build_container_image(args: BuildContainerImageArgs) -> anyhow::Result<()> { + let BuildContainerImageArgs { + container_engine, + no_cache, + push, + tags, + target, + } = args; + + let triple: Triple = match target { + Some(target) => target.into(), + None => target_lexicon::HOST, + }; + + let tags = if tags.is_empty() { + vec![triple.default_container_tag()] + } else { + tags + }; + + match triple.dockerfile() { + Some(dockerfile) => { + let container_engine = container_engine.unwrap_or(ContainerEngine::autodetect()?); + + let mut cmd = Command::new(container_engine.to_string()); + cmd.args([OsStr::new("buildx"), OsStr::new("build")]); + for tag in tags.iter() { + cmd.args([OsStr::new("-t"), tag]); + } + cmd.args([OsStr::new("-f"), &dockerfile, OsStr::new(".")]) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + if no_cache { + cmd.arg(OsStr::new("--no-cache")); + } + println!("{cmd:?}"); + if !cmd.status()?.success() { + return Err(ContainerError::ContainerImageBuild.into()); + } + + if push { + for tag in tags.iter() { + push_image(&container_engine, tag)?; + } + } + + Ok(()) + } + None => Err(ContainerError::UnsupportedTarget(triple.to_string()).into()), + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs new file mode 100644 index 0000000..617dce4 --- /dev/null +++ b/xtask/src/main.rs @@ -0,0 +1,32 @@ +use clap::{Parser, Subcommand}; + +mod containers; + +use crate::containers::{build_container_image, BuildContainerImageArgs}; + +/// The `xtask` CLI. +#[derive(Parser)] +#[command(author, version, about, long_about = None)] +struct Cli { + /// Subcommands + #[command(subcommand)] + command: Commands, +} + +#[derive(Subcommand)] +enum Commands { + /// Build container image. + BuildContainerImage(BuildContainerImageArgs), +} + +fn main() -> anyhow::Result<()> { + let cli = Cli::parse(); + + match cli.command { + Commands::BuildContainerImage(args) => { + build_container_image(args)?; + } + } + + Ok(()) +}