10 Commits

Author SHA1 Message Date
iretq f9b6036c95 get rid of ctx.prefix and add run cwd kwarg 2026-05-20 20:52:42 +02:00
marv7000 16d81f509f things 2026-05-20 00:30:38 +02:00
Marvin Friedrich 312750c61b More stuff, didn't test 2026-05-19 18:12:11 +02:00
marv7000 b71906f402 idk 2026-05-19 03:24:20 +02:00
marv7000 45d47e8d84 wip3 2026-05-19 03:24:10 +02:00
iretq 0c9a3fde94 wip 2 2026-05-18 20:39:21 +02:00
iretq 0d610fd2de wip 2026-05-18 19:32:33 +02:00
marv7000 98f3fee099 More stuff 2026-05-18 00:49:24 +02:00
marv7000 695f30d678 first 2026-05-17 23:23:13 +02:00
marv7000 a23e2c83d1 Initial commit 2026-04-11 20:18:09 +02:00
50 changed files with 18250 additions and 1560 deletions
+4
View File
@@ -0,0 +1,4 @@
target
build
.git
*.bak
+2
View File
@@ -1,2 +1,4 @@
/target
/build
*.bak
*.lock
Generated
+1134 -47
View File
File diff suppressed because it is too large Load Diff
+19 -10
View File
@@ -1,15 +1,24 @@
[package]
name = "builder"
name = "distro"
version = "0.1.0"
edition = "2024"
license = "MIT"
[dependencies]
allocative = "0.3.4"
anyhow = "1.0.102"
clap = { version = "4.6.1", features = ["derive"] }
either = "1.16.0"
petgraph = "0.8.3"
smallvec = "1.15.1"
starlark = "0.13.0"
starlark_derive = "0.13.0"
thiserror = "2.0.18"
anyhow = "1.0"
clap = { version = "4.5", features = ["derive"] }
hex = "0.4"
libc = "0.2"
reqwest = { version = "0.12", default-features = false, features = [
"blocking",
"rustls-tls",
] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
sha2 = "0.10"
shell-escape = "0.1"
starlark = "0.13"
starlark_derive = "0.13"
allocative = "0.3"
tempfile = "3.10"
walkdir = "2.5"
+46
View File
@@ -0,0 +1,46 @@
FROM alpine:edge
RUN apk upgrade --no-cache && apk add --no-cache \
alpine-sdk \
apk-tools \
autoconf \
automake \
bash \
bc \
bison \
bzip2 \
ca-certificates \
cmake \
coreutils \
curl \
file \
findutils \
flex \
gawk \
gettext-dev \
git \
grep \
gzip \
elfutils-dev \
gmp-dev \
mpfr-dev \
mpc1-dev \
libtool \
linux-headers \
meson \
mtools \
nasm \
ncurses \
ninja \
openssl \
openssl-dev \
patch \
pkgconf \
python3 \
rsync \
tar \
texinfo \
xz \
zstd
WORKDIR /work
+18
View File
@@ -0,0 +1,18 @@
MIT License
Copyright (c) 2026 marv7000
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
+31 -45
View File
@@ -1,64 +1,50 @@
container_runtime = "podman"
container_image = "local/distro-builder:latest"
container_dockerfile = "Dockerfile"
arch = "x86_64"
libc = "glibc"
if libc == "glibc":
env = "gnu"
elif libc == "musl":
env = "musl"
else:
fail(f"Unknown libc: {libc}")
env = libc
prefix = path("/usr")
host_cflags = "-O2 -pipe"
host_cxxflags = host_cflags
host_ldflags = "-Wl,-O1 -Wl,--sort-common -Wl,--as-needed"
host_cflags = ["-O2", "-pipe"]
host_cxxflags = host_cflags + []
host_ldflags = ["-Wl,-O1", "-Wl,--sort-common", "-Wl,--as-needed"]
target_cflags = host_cflags + []
target_cxxflags = host_cxxflags + []
target_ldflags = host_ldflags + ["-Wl,-z,now"]
target_cflags = host_cflags
target_cxxflags = host_cxxflags
target_ldflags = host_ldflags + " -Wl,-z,now"
if arch == "x86_64":
flags = [
"-march=x86-64-v3",
"-mtune=generic",
"-fstack-clash-protection",
"-fstack-protector-strong",
"-fcf-protection",
]
flags = " -march=x86-64-v3 -mtune=generic -fstack-clash-protection -fstack-protector-strong -fcf-protection"
target_cflags += flags
target_cxxflags += flags
target_ldflags += ["-Wl,-z,pack-relative-relocs"]
config(
arch = arch,
recipes_dir = path("./recipes"),
host_recipes_dir = path("./host-recipes"),
container = podman(
image = "local/builder:latest",
dockerfile = path("./Dockerfile"),
),
target_ldflags += " -Wl,-z,pack-relative-relocs"
options = dict(
target_arch = arch,
target_triple = f"{arch}-orchid-linux-{env}",
target_triple = f"{arch}-linux-{env}",
host_cflags = host_cflags,
host_cxxflags = host_cxxflags,
host_ldflags = host_ldflags,
cflags = target_cflags,
cxxflags = target_cxxflags,
ldflags = target_ldflags,
libc = libc,
prefix = prefix,
bindir = prefix / "bin",
sbindir = prefix / "bin",
libdir = prefix / "lib",
libexecdir = prefix / "libexec",
includedir = prefix / "include",
sysconfdir = path("/etc"),
localstatedir = path("/var"),
host_cflags = " ".join(host_cflags),
host_cxxflags = " ".join(host_cxxflags),
host_ldflags = " ".join(host_ldflags),
cflags = " ".join(target_cflags),
cxxflags = " ".join(target_cxxflags),
ldflags = " ".join(target_ldflags),
prefix = "/usr",
bindir = "/usr/bin",
sbindir = "/usr/bin",
libdir = "/usr/lib",
libexecdir = "/usr/libexec",
includedir = "/usr/include",
sysconfdir = "/etc",
localstatedir = "/var",
)
+7 -5
View File
@@ -4,18 +4,18 @@ metadata = meta(
description = "GNU binutils cross-compiled for the target triple",
license = "GPL-3.0-or-later",
)
source = tarball(
source = tarball_source(
url = f"https://ftp.gnu.org/gnu/binutils/binutils-{version}.tar.xz",
sha256 = "d75a94f4d73e7a4086f7513e67e439e8fcdcbb726ffe63f4661744e6256b2cf2",
strip_components = 1,
)
def configure(ctx):
ctx.run(
configure_args = [
ctx.source_dir / "configure",
"--prefix=" + options.prefix,
"--prefix=/",
"--target=" + options.target_triple,
"--with-sysroot=" + ctx.sysroot_dir,
"--with-sysroot=" + ctx.sysroot,
"--with-pic",
"--enable-cet",
"--enable-default-execstack=no",
@@ -30,7 +30,9 @@ def configure(ctx):
"--disable-werror",
# gprofng's libcollector relies on glibc-specific internals.
"--disable-gprofng",
env = {
]
ctx.run(configure_args, env = {
"CFLAGS": options.host_cflags,
"CXXFLAGS": options.host_cxxflags,
"LDFLAGS": options.host_ldflags,
+47
View File
@@ -0,0 +1,47 @@
version = "16.1.0"
revision = 1
metadata = meta(
description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)",
license = "GPL-3.0-or-later",
)
source = tarball_source(
url = f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
sha256 = "50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
strip_components = 1,
)
host_deps = ["binutils"]
def configure(ctx):
ctx.run([
ctx.source_dir / "configure",
"--target=" + options.target_triple,
"--prefix=/",
"--with-sysroot=" + ctx.sysroot,
"--without-headers",
"--with-newlib",
"--enable-languages=c,c++",
"--enable-default-pie",
"--enable-default-ssp",
"--disable-nls",
"--disable-shared",
"--disable-threads",
"--disable-libssp",
"--disable-libgomp",
"--disable-libquadmath",
"--disable-libatomic",
"--disable-libvtv",
"--disable-multilib",
], env = {
"CFLAGS": options.host_cflags,
"CXXFLAGS": options.host_cxxflags,
"LDFLAGS": options.host_ldflags,
})
def build(ctx):
jobs = "-j" + str(ctx.jobs)
ctx.run(["make", jobs, "all-gcc"])
ctx.run(["make", jobs, "all-target-libgcc"])
def install(ctx, pkg):
ctx.run(["make", "install-gcc"], env = {"DESTDIR": pkg.dest_dir})
ctx.run(["make", "install-target-libgcc"], env = {"DESTDIR": pkg.dest_dir})
+53
View File
@@ -0,0 +1,53 @@
version = "16.1.0"
revision = 1
metadata = meta(
description = "GNU GCC cross-compiler targeting the system triple",
license = "GPL-3.0-or-later",
website = "https://gcc.gnu.org/",
)
source = tarball_source(
url = f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
sha256 = "50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
strip_components = 1,
)
host_deps = ["binutils", "gcc-bootstrap"]
deps = [options.libc, "linux-headers"]
def configure(ctx):
ctx.run([
ctx.source_dir / "configure",
"--target=" + options.target_triple,
"--prefix=/",
"--with-sysroot=" + ctx.sysroot,
"--with-build-sysroot=" + ctx.sysroot,
"--enable-languages=c,c++,lto",
"--disable-bootstrap",
"--enable-default-pie",
"--enable-default-ssp",
"--enable-lto",
"--enable-threads=posix",
"--enable-tls",
"--enable-libstdcxx-time",
"--enable-checking=release",
"--enable-cet=auto",
"--enable-linker-build-id",
"--disable-nls",
"--disable-multilib",
"--disable-fixed-point",
"--disable-werror",
"--disable-libsanitizer",
"--disable-symvers",
], env = {
"CFLAGS": options.host_cflags,
"CXXFLAGS": options.host_cxxflags,
"LDFLAGS": options.host_ldflags,
})
def build(ctx):
ctx.run(["make", "-j" + str(ctx.jobs)])
def install(ctx, pkg):
ctx.run(["make", "install-strip"], env = {"DESTDIR": pkg.dest_dir})
# Drop libtool archives.
ctx.run(["find", pkg.dest_dir, "-name", "*.la", "-delete"])
+52
View File
@@ -0,0 +1,52 @@
version = "20.1.0"
revision = 1
metadata = meta(
description = "LLVM compiler infrastructure with clang and lld",
license = "Apache-2.0 WITH LLVM-exception",
website = "https://llvm.org/",
)
source = tarball_source(
url = f"https://github.com/llvm/llvm-project/releases/download/llvmorg-{version}/llvm-project-{version}.src.tar.xz",
sha256 = "?",
strip_components = 1,
)
host_deps = ["binutils"]
def configure(ctx):
ctx.run([
"cmake",
"-S", ctx.source_dir / "llvm",
"-B", ctx.build_dir,
"-G", "Ninja",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_INSTALL_PREFIX=/",
"-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra;lld",
"-DLLVM_ENABLE_RUNTIMES=compiler-rt",
"-DLLVM_TARGETS_TO_BUILD=X86;AArch64;RISCV",
"-DLLVM_DEFAULT_TARGET_TRIPLE=" + options.target_triple,
"-DLLVM_HOST_TRIPLE=" + options.target_triple,
"-DLLVM_ENABLE_LIBXML2=OFF",
"-DLLVM_ENABLE_LIBEDIT=OFF",
"-DLLVM_ENABLE_TERMINFO=OFF",
"-DLLVM_ENABLE_ASSERTIONS=OFF",
"-DLLVM_ENABLE_PIC=ON",
"-DLLVM_BUILD_LLVM_DYLIB=ON",
"-DLLVM_LINK_LLVM_DYLIB=ON",
"-DLLVM_INSTALL_UTILS=ON",
"-DLLVM_INCLUDE_TESTS=OFF",
"-DLLVM_INCLUDE_EXAMPLES=OFF",
"-DLLVM_INCLUDE_BENCHMARKS=OFF",
"-DCLANG_DEFAULT_LINKER=lld",
"-DCLANG_DEFAULT_RTLIB=compiler-rt",
"-DCLANG_DEFAULT_CXX_STDLIB=libstdc++",
], env = {
"CFLAGS": options.host_cflags,
"CXXFLAGS": options.host_cxxflags,
"LDFLAGS": options.host_ldflags,
})
def build(ctx):
ctx.run(["cmake", "--build", ctx.build_dir, "-j" + str(ctx.jobs)])
def install(ctx, pkg):
ctx.run(["cmake", "--install", ctx.build_dir], env = {"DESTDIR": pkg.dest_dir})
+95
View File
@@ -0,0 +1,95 @@
# Autotools
def autotools_configure(ctx, extra_args = [], extra_env = {}):
env = {
"CFLAGS": options.cflags,
"CXXFLAGS": options.cxxflags,
"LDFLAGS": options.ldflags,
}
env.update(extra_env)
ctx.run([
ctx.source_dir / "configure",
"--host=" + options.target_triple,
"--with-sysroot=" + ctx.sysroot,
"--prefix=" + options.prefix,
"--sysconfdir=" + options.sysconfdir,
"--localstatedir=" + options.localstatedir,
"--bindir=" + options.bindir,
"--sbindir=" + options.sbindir,
"--libdir=" + options.libdir,
"--disable-static",
"--enable-shared",
] + extra_args, env = env)
def autotools_build(ctx, extra_args = []):
ctx.run(["make", "-j" + str(ctx.jobs)] + extra_args)
def autotools_install(ctx, pkg, extra_args = []):
ctx.run(["make", "install"] + extra_args, env = { "DESTDIR": pkg.dest_dir })
def autotools(configure_args = [], configure_env = {}, build_args = [], install_args = []):
def _configure(ctx):
autotools_configure(ctx, extra_args = configure_args, extra_env = configure_env)
def _build(ctx):
autotools_build(ctx, extra_args = build_args)
def _install(ctx, pkg):
autotools_install(ctx, pkg, extra_args = install_args)
return _configure, _build, _install
# CMake
def cmake_configure(ctx, extra_args = [], extra_env = {}, host = False):
if host:
env = {
"CFLAGS": options.host_cflags,
"CXXFLAGS": options.host_cxxflags,
"LDFLAGS": options.host_ldflags,
}
toolchain_args = []
else:
env = {
"CFLAGS": options.cflags,
"CXXFLAGS": options.cxxflags,
"LDFLAGS": options.ldflags,
}
toolchain_args = [
"-DCMAKE_SYSTEM_NAME=Linux",
"-DCMAKE_SYSTEM_PROCESSOR=" + options.target_arch,
"-DCMAKE_SYSROOT=" + ctx.sysroot,
"-DCMAKE_C_COMPILER=" + options.target_triple + "-gcc",
"-DCMAKE_CXX_COMPILER=" + options.target_triple + "-g++",
"-DCMAKE_FIND_ROOT_PATH_MODE_PROGRAM=NEVER",
"-DCMAKE_FIND_ROOT_PATH_MODE_LIBRARY=ONLY",
"-DCMAKE_FIND_ROOT_PATH_MODE_INCLUDE=ONLY",
"-DCMAKE_FIND_ROOT_PATH_MODE_PACKAGE=ONLY",
]
env.update(extra_env)
ctx.run([
"cmake",
"-S", ctx.source_dir,
"-B", ctx.build_dir,
"-G", "Ninja",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_INSTALL_PREFIX=" + options.prefix,
"-DCMAKE_INSTALL_SYSCONFDIR=" + options.sysconfdir,
"-DCMAKE_INSTALL_LOCALSTATEDIR=" + options.localstatedir,
] + toolchain_args + extra_args, env = env)
def cmake_build(ctx, extra_args = []):
ctx.run(["cmake", "--build", ctx.build_dir, "-j", str(ctx.jobs)] + extra_args)
def cmake_install(ctx, pkg, extra_args = []):
ctx.run(
["cmake", "--install", ctx.build_dir] + extra_args,
env = {"DESTDIR": pkg.dest_dir},
)
def cmake(configure_args = [], configure_env = {}, build_args = [], install_args = [], host = False):
def _configure(ctx):
cmake_configure(ctx, extra_args = configure_args, extra_env = configure_env, host = host)
def _build(ctx):
cmake_build(ctx, extra_args = build_args)
def _install(ctx, pkg):
cmake_install(ctx, pkg, extra_args = install_args)
return _configure, _build, _install
+22
View File
@@ -0,0 +1,22 @@
version = "5.2.32"
revision = 1
metadata = meta(
description = "GNU Bourne-Again SHell",
license = "GPL-3.0-or-later",
website = "https://www.gnu.org/software/bash/",
)
source = tarball_source(
url = f"https://ftp.gnu.org/gnu/bash/bash-{version}.tar.gz",
sha256 = "d3ef80d2b67d8cbbe4d3265c63a72c46f9b278ead6e0e06d61801b58f23f50b5",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = [options.libc, "ncurses", "readline"]
configure, build, install = autotools(configure_args = [
"--without-bash-malloc",
"--with-installed-readline",
"--enable-readline",
"--enable-history",
"--enable-job-control",
], configure_env = {"CFLAGS": "-std=gnu17"})
+25
View File
@@ -0,0 +1,25 @@
version = "9.6"
revision = 1
metadata = meta(
description = "GNU core utilities (file, shell, and text manipulation)",
license = "GPL-3.0-or-later",
website = "https://www.gnu.org/software/coreutils/",
)
source = tarball_source(
url = f"https://ftp.gnu.org/gnu/coreutils/coreutils-{version}.tar.xz",
sha256 = "7a0124327b398fd9eb1a6abde583389821422c744ffa10734b24f557610d3283",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = [options.libc]
configure, build, install = autotools(
configure_args = [
"--enable-no-install-program=kill,uptime",
"--without-selinux",
"--without-openssl",
],
# coreutils' configure runs link tests that require a working executable;
# cross builds need this hint to skip a known false positive.
configure_env = {"FORCE_UNSAFE_CONFIGURE": "1"},
)
+36
View File
@@ -0,0 +1,36 @@
version = "2.41"
revision = 1
metadata = meta(
description = "GNU C library",
license = "LGPL-2.1-or-later",
website = "https://www.gnu.org/software/libc/",
)
source = tarball_source(
url = f"https://ftp.gnu.org/gnu/glibc/glibc-{version}.tar.xz",
sha256 = "a5a26b22f545d6b7d7b3dd828e11e428f24f4fac43c934fb071b6a7d0828e901",
strip_components = 1,
)
host_deps = ["binutils", "gcc-bootstrap"]
deps = ["linux-headers"]
build_if = options.libc == "glibc"
def configure(ctx):
autotools_configure(ctx, [
"--build=" + options.target_triple,
"--with-headers=" + ctx.sysroot / options.prefix / "include",
"--enable-kernel=5.4",
"--enable-bind-now",
"--enable-stack-protector=strong",
"--enable-cet",
"--disable-werror",
"--disable-profile",
"--disable-nscd",
"--without-selinux",
"--without-gd",
"libc_cv_slibdir=/lib",
"libc_cv_rtlddir=/lib",
"libc_cv_forced_unwind=yes",
])
_, build, install = autotools()
+51
View File
@@ -0,0 +1,51 @@
version = "12.2.0"
revision = 1
metadata = meta(
description = "Modern, secure, portable, multiprotocol bootloader and boot manager",
license = "BSD-2-Clause",
website = "https://limine-bootloader.org"
)
source = tarball_source(
url = f"https://github.com/Limine-Bootloader/Limine/releases/download/v{version}/limine-{version}.tar.gz",
sha256 = "db8a119878cfeead63c0a78236c577c40539c5759496950ea0ed32a6cf567865",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = [options.libc]
build_if = options.target_arch in ["x86_64", "aarch64", "riscv64", "loongarch64"]
arch_configure_args = {
"x86_64": ["--enable-uefi-x86-64", "--enable-uefi-ia32", "--enable-bios", "--enable-bios-cd"],
"aarch64": ["--enable-uefi-aarch64"],
"riscv64": ["--enable-uefi-riscv64"],
"loongarch64": ["--enable-uefi-loongarch64"],
}
configure, build, install = autotools(
configure_args = ["--enable-uefi-cd"] + arch_configure_args.get(options.target_arch),
configure_env = {"TOOLCHAIN_FOR_TARGET": options.target_triple + "-"},
)
subpackages = [
subpackage(
"limine-uefi",
meta(description = "UEFI files"),
[
"usr/share/limine/BOOT*.EFI",
"usr/share/limine/limine-uefi-*.bin",
],
),
]
if options.target_arch == "x86_64":
subpackages += [
subpackage(
"limine-bios",
meta(description = "BIOS files"),
[
"usr/share/limine/limine-bios*",
],
)
]
+7 -7
View File
@@ -4,17 +4,17 @@ metadata = meta(
description = "Linux kernel headers for userspace development",
license = "GPL-2.0-only",
)
source = tarball(
source = tarball_source(
url = f"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{version}.tar.xz",
sha256 = "ac07acdf76cf4621cc5187a2670270a1a699533c8a6b225e4878c416ad83f1c4",
strip_components = 1,
)
def build(ctx):
ctx.run("cp", "-rp", ctx.source_dir / ".", ctx.build_dir)
ctx.run("make", "headers_install", "ARCH=" + options.target_arch)
ctx.run("find", ctx.build_dir / "usr" / "include", "-type", "f", "!", "-name", "*.h", "-delete")
ctx.run(["cp", "-rp", ctx.source_dir / ".", ctx.build_dir])
ctx.run(["make", "headers_install", "ARCH=" + options.target_arch])
ctx.run(["find", ctx.build_dir / "usr/include", "-type", "f", "!", "-name", "*.h", "-delete"])
def install(ctx):
ctx.run("mkdir", "-p", ctx.dest_dir / options.prefix)
ctx.run("cp", "-rp", ctx.build_dir / "usr" / "include", ctx.dest_dir / options.prefix)
def install(ctx, pkg):
ctx.run(["mkdir", "-p", pkg.dest_dir / options.prefix])
ctx.run(["cp", "-rp", ctx.build_dir / "usr/include", pkg.dest_dir / options.prefix])
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
version = "7.0.9"
revision = 1
metadata = meta(
description = "Linux kernel",
license = "GPL-2.0-only",
)
source = tarball_source(
url = f"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{version}.tar.xz",
sha256 = "ac07acdf76cf4621cc5187a2670270a1a699533c8a6b225e4878c416ad83f1c4",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
def make_args(ctx, *args):
# Translate arch name
if options.target_arch == "aarch64":
linux_arch = "arm64"
else:
linux_arch = options.target_arch
result = [
"make",
"ARCH=" + linux_arch,
"CROSS_COMPILE=" + options.target_triple + "-",
"-j" + str(ctx.jobs),
]
result.extend(args)
return result
def configure(ctx):
ctx.run(["cp", "-rp", ctx.source_dir / ".", ctx.build_dir])
ctx.run(["cp", ctx.files / "config." + options.target_arch, ctx.build_dir / ".config"])
ctx.run(make_args(ctx, "olddefconfig"))
def build(ctx):
ctx.run(make_args(ctx))
def install(ctx, pkg):
ctx.install(
ctx.build_dir + "/arch/x86/boot/bzImage",
pkg.destdir + "/boot/vmlinuz-" + version,
)
+18
View File
@@ -0,0 +1,18 @@
version = "4.4.1"
revision = 1
metadata = meta(
description = "GNU make build automation tool",
license = "GPL-3.0-or-later",
website = "https://www.gnu.org/software/make/",
)
source = tarball_source(
url = f"https://ftp.gnu.org/gnu/make/make-{version}.tar.gz",
sha256 = "dd16fb1d67bfab79a72f5e8390735c49e3e8e70b4945a15ab1f81ddb78658fb3",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = [options.libc]
configure, build, install = autotools(configure_args = [
"--without-guile",
])
+27
View File
@@ -0,0 +1,27 @@
version = "1.2.6"
revision = 1
metadata = meta(
description = "Small, standards-conformant implementation of libc",
license = "MIT",
)
source = tarball_source(
url = f"https://musl.libc.org/releases/musl-{version}.tar.gz",
sha256 = "d585fd3b613c66151fc3249e8ed44f77020cb5e6c1e635a616d3f9f82460512a",
strip_components = 1,
)
host_deps = ["binutils", "gcc-bootstrap"]
build_if = options.libc == "musl"
def configure(ctx):
ctx.run([
ctx.source_dir / "configure",
"--target=" + options.target_triple,
"--prefix=" + options.prefix,
"--syslibdir=/lib",
], env = {
"CC": options.target_triple + "-gcc",
"CFLAGS": options.cflags,
"LDFLAGS": options.ldflags,
})
_, build, install = autotools()
+38
View File
@@ -0,0 +1,38 @@
version = "6.5"
revision = 1
metadata = meta(
description = "Terminal control library with wide-character support",
license = "MIT",
website = "https://invisible-island.net/ncurses/",
)
source = tarball_source(
url = f"https://invisible-mirror.net/archives/ncurses/ncurses-{version}.tar.gz",
sha256 = "136d91bc269a9a5785e5f9e980bc76ab57428f604ce3e5a5a90cebc767971cc6",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = [options.libc]
configure, build, _ = autotools(configure_args = [
"--with-shared",
"--without-debug",
"--without-ada",
"--enable-pc-files",
"--enable-widec",
"--with-termlib",
"--with-cxx-binding",
"--with-cxx-shared",
"--with-pkg-config-libdir=/usr/lib/pkgconfig",
"--mandir=/usr/share/man",
], configure_env = {
# Conflicts with GCC 16 headers
"cf_cv_type_of_bool": "bool",
"cf_cv_cc_bool_type": "1",
"cf_cv_builtin_bool": "1",
"ac_cv_header_stdbool_h": "yes",
})
def install(ctx, pkg):
autotools_install(ctx, pkg, extra_args = [
"DESTDIR=" + pkg.dest_dir
])
+51
View File
@@ -0,0 +1,51 @@
version = "3.4.1"
revision = 1
metadata = meta(
description = "Cryptography and TLS library (OpenSSL)",
license = "Apache-2.0",
website = "https://www.openssl.org/",
)
source = tarball_source(
url = f"https://github.com/openssl/openssl/releases/download/openssl-{version}/openssl-{version}.tar.gz",
sha256 = "?",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = ["zlib"]
def configure(ctx):
# OpenSSL uses its own perl-based Configure script. The first argument is
# the OpenSSL "target" — pick the one matching our triple.
if options.target_arch == "x86_64":
ossl_target = "linux-x86_64"
elif options.target_arch == "aarch64":
ossl_target = "linux-aarch64"
elif options.target_arch == "riscv64":
ossl_target = "linux64-riscv64"
else:
fail("openssl: unsupported target_arch " + options.target_arch)
ctx.run([
ctx.source_dir / "Configure",
ossl_target,
"--prefix=" + options.prefix,
"--openssldir=/etc/ssl",
"--libdir=lib",
"shared",
"zlib",
"no-tests",
"no-static-engine",
"enable-ktls",
], env = {
"CC": options.target_triple + "-gcc",
"AR": options.target_triple + "-ar",
"RANLIB": options.target_triple + "-ranlib",
"CFLAGS": options.cflags,
"LDFLAGS": options.ldflags,
})
def build(ctx):
ctx.run(["make", "-j" + str(ctx.jobs)])
def install(ctx, pkg):
ctx.run(["make", "install_sw", "install_ssldirs"], env = {"DESTDIR": pkg.dest_dir})
+19
View File
@@ -0,0 +1,19 @@
version = "3.3.3"
revision = 1
metadata = meta(
description = "Lightweight pkg-config implementation",
license = "ISC",
website = "http://pkgconf.org/",
)
source = tarball_source(
url = f"https://distfiles.ariadne.space/pkgconf/pkgconf-{version}.tar.xz",
sha256 = "?",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = [options.libc]
configure, build, install = autotools(configure_args = [
"--with-system-libdir=" + options.libdir,
"--with-system-includedir=" + options.includedir,
])
+29
View File
@@ -0,0 +1,29 @@
version = "8.2"
revision = 1
metadata = meta(
description = "Library for command-line editing",
license = "GPL-3.0-or-later",
website = "https://tiswww.case.edu/php/chet/readline/rltop.html",
)
source = tarball_source(
url = f"https://ftp.gnu.org/gnu/readline/readline-{version}.tar.gz",
sha256 = "3feb7171f16a84ee82ca18a36d7b9be109a52c04f492a053331d7d1095007c35",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = [options.libc, "ncurses"]
configure, build, _ = autotools(
configure_args = ["--with-curses"],
configure_env = {
# Force linking against the system curses; otherwise readline's
# configure may pick a static libtermcap stub it ships internally.
"bash_cv_termcap_lib": "ncursesw",
},
)
# Readline overwrites DESTDIR
def install(ctx, pkg):
autotools_install(ctx, pkg, extra_args = [
"DESTDIR=" + pkg.dest_dir
])
+18
View File
@@ -0,0 +1,18 @@
version = "5.6.3"
revision = 1
metadata = meta(
description = "XZ Utils — LZMA/XZ compression tools and library",
license = "0BSD AND GPL-2.0-or-later AND LGPL-2.1-or-later",
website = "https://tukaani.org/xz/",
)
source = tarball_source(
url = f"https://github.com/tukaani-project/xz/releases/download/v{version}/xz-{version}.tar.xz",
sha256 = "db0590629b6f0fa36e74aea5f9731dc6f8df068ce7b7bafa45301832a5eebc3a",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = [options.libc]
configure, build, install = autotools(configure_args = [
"--disable-doc",
])
+36
View File
@@ -0,0 +1,36 @@
version = "1.3.2"
revision = 1
metadata = meta(
description = "Lossless data-compression library",
license = "Zlib",
website = "https://zlib.net/",
)
source = tarball_source(
url = f"https://zlib.net/zlib-{version}.tar.xz",
sha256 = "d7a0654783a4da529d1bb793b7ad9c3318020af77667bcae35f95d0e42a792f3",
strip_components = 1,
)
host_deps = ["binutils", "gcc"]
deps = [options.libc]
def configure(ctx):
# zlib ships its own ./configure that does not understand the usual
# autoconf flags (no --host, --build, etc.), so it is invoked directly.
ctx.run([
ctx.source_dir / "configure",
"--prefix=" + options.prefix,
"--libdir=" + options.libdir,
"--sharedlibdir=" + options.libdir,
], env = {
"CC": options.target_triple + "-gcc",
"AR": options.target_triple + "-ar",
"RANLIB": options.target_triple + "-ranlib",
"CFLAGS": options.cflags,
"LDFLAGS": options.ldflags,
})
def build(ctx):
ctx.run(["make", "-j" + str(ctx.jobs)])
def install(ctx, pkg):
ctx.run(["make", "install"], env = {"DESTDIR": pkg.dest_dir})
+1143
View File
File diff suppressed because it is too large Load Diff
+23 -83
View File
@@ -1,27 +1,13 @@
use crate::{
container::{ContainerManager, PodmanRuntime},
eval::{
Config, ContainerConfig, ContainerManagerWrapper, Context, Path, config_globals,
eval_files, types_globals,
},
log,
plan::{Plan, PlanKey},
recipe::RecipeSet,
};
use clap::{Parser, Subcommand};
use starlark::{
environment::{GlobalsBuilder, Module},
eval,
values::Value,
};
use std::{cell::Cell, path::PathBuf, sync::Arc};
use std::path::PathBuf;
use crate::{builder::Builder, config::Config, eval, recipe::RecipeSet};
#[derive(Debug, Parser)]
struct Cli {
#[arg(
long,
short = 'C',
short,
default_value = ".",
help = "Directory containing the configuration and recipe files"
)]
@@ -60,78 +46,32 @@ struct BuildCommand {
enum Command {
Fetch(FetchCommand),
Build(BuildCommand),
#[command(about = "Create or refresh the configured build container image")]
Image,
}
pub fn run() -> anyhow::Result<()> {
let cli = Cli::parse();
let root_path = cli.root.canonicalize().unwrap_or(cli.root);
let config: Config = {
let cell = Cell::new(None);
let config_path = root_path.join("config.star");
eval_files(
&[&config_path],
&GlobalsBuilder::standard()
.with(types_globals)
.with(config_globals)
.build(),
None,
None,
Some(&cell),
)?;
cell.take()
.ok_or_else(|| anyhow::anyhow!("`config` was not called"))?
};
let container_runtime = match config.container() {
ContainerConfig::Podman(_) => Arc::new(PodmanRuntime::new()?),
};
let container_manager = ContainerManager::new(container_runtime);
let mut recipes = RecipeSet::new(&config);
recipes.load_recipes(
&root_path.join(config.recipes_dir()),
&root_path.join(config.host_recipes_dir()),
)?;
// let wrapper = ContainerManagerWrapper(&container_manager);
// for (name, recipe) in recipes.packages.iter() {
// println!("{name}: {:#?}", recipe);
// let mo = Module::new();
// let mut eval = eval::Evaluator::new(&mo);
// eval.extra = Some(&wrapper);
// eval.eval_function(
// recipe.build.unwrap().0.to_value(),
// &[mo.heap().alloc(Context {
// source_dir: Path::new("/source"),
// build_dir: Path::new("/build"),
// jobs: 4,
// })],
// &[],
// )
// .unwrap();
// }
let mut plan = Plan::new(&recipes);
let config = Config::load(&root_path.join("config.star"))?;
match cli.command {
Command::Fetch(_) => {}
Command::Build(cmd) => {
for recipe in cmd.recipes.iter() {
plan.add_wanted(if let Some(recipe) = recipe.strip_prefix("host:") {
PlanKey::ToolInstall(recipe.to_string())
} else {
PlanKey::PkgPackage(recipe.clone())
});
Command::Fetch(command) => {
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
let mut builder = Builder::new(root_path, config);
builder.fetch(&recipes, &command.recipes, command.dry_run)
}
Command::Build(command) => {
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
let mut builder = Builder::new(root_path, config);
builder.build(&recipes, &command.recipes, command.rebuild, command.dry_run)
}
Command::Image => {
let mut builder = Builder::new(root_path, config);
builder.ensure_container_ready()
}
}
}
log!("plan", "{:#?}", plan.steps());
Ok(())
}
+117
View File
@@ -0,0 +1,117 @@
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use starlark::values::dict::FrozenDictRef;
use crate::{
eval::{ExtractError, eval_file, extract_string},
options::Options,
};
#[derive(Debug)]
pub enum ContainerRuntime {
Docker,
Podman,
}
impl ContainerRuntime {
pub fn as_str(&self) -> &'static str {
match self {
Self::Docker => "docker",
Self::Podman => "podman",
}
}
}
impl std::fmt::Display for ContainerRuntime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl TryFrom<&str> for ContainerRuntime {
type Error = anyhow::Error;
fn try_from(value: &str) -> anyhow::Result<Self> {
match value {
"docker" => Ok(Self::Docker),
"podman" => Ok(Self::Podman),
_ => anyhow::bail!("invalid runtime: {value}"),
}
}
}
#[derive(Debug)]
pub struct Config {
pub container_runtime: ContainerRuntime,
pub container_image: String,
pub container_dockerfile: PathBuf,
pub arch: String,
pub options: Options,
}
impl Config {
pub fn load(path: &Path) -> anyhow::Result<Self> {
let module = eval_file(path, None, None)?;
let container_runtime = match extract_string(&module, "container_runtime") {
Ok(v) => ContainerRuntime::try_from(v.as_str())?,
Err(ExtractError::NotFound) => ContainerRuntime::Podman,
Err(ExtractError::TypeMismatch) => anyhow::bail!("`container_runtime` is not a string"),
};
let container_image = match extract_string(&module, "container_image") {
Ok(container_image) => container_image,
Err(ExtractError::NotFound) => {
anyhow::bail!("`container_image` config variable not set")
}
Err(ExtractError::TypeMismatch) => anyhow::bail!("`container_image` is not a string"),
};
let container_dockerfile = match extract_string(&module, "container_dockerfile") {
Ok(container_dockerfile) => PathBuf::from(container_dockerfile),
Err(ExtractError::NotFound) => PathBuf::from("Dockerfile"),
Err(ExtractError::TypeMismatch) => {
anyhow::bail!("`container_dockerfile` is not a string")
}
};
let arch = match extract_string(&module, "arch") {
Ok(arch) => arch,
Err(ExtractError::NotFound) => anyhow::bail!("`arch` config variable not set"),
Err(ExtractError::TypeMismatch) => anyhow::bail!("`arch` is not a string"),
};
let frozen_module = module.freeze()?;
let options_value = frozen_module
.get_option("options")?
.ok_or_else(|| anyhow::anyhow!("`options` config variable not set"))?;
let entries = {
// SAFETY: the FrozenValue is only used to construct a FrozenDictRef whose
// lifetime is bounded by `options_value`, which keeps the frozen heap alive.
let dict =
FrozenDictRef::from_frozen_value(unsafe { options_value.unchecked_frozen_value() })
.ok_or_else(|| anyhow::anyhow!("`options` is not a dict"))?;
dict.iter()
.map(|(k, v)| {
let key = k
.to_value()
.unpack_str()
.ok_or_else(|| anyhow::anyhow!("non-string key in `options`"))?
.to_owned();
Ok((key, options_value.map(|_| v)))
})
.collect::<anyhow::Result<HashMap<_, _>>>()?
};
let options = Options::new(entries);
Ok(Self {
container_runtime,
container_image,
container_dockerfile,
arch,
options,
})
}
}
+292
View File
@@ -0,0 +1,292 @@
use std::{
fs::File,
io::{self, Read},
os::{fd::FromRawFd, unix::io::RawFd},
path::{Path, PathBuf},
process::{Command, Stdio},
ptr,
sync::{
Mutex,
atomic::{AtomicBool, AtomicI32, Ordering},
},
thread,
};
use anyhow::{Context, bail};
use crate::config::ContainerRuntime;
#[derive(Debug, Clone)]
pub struct Mount {
pub host: PathBuf,
pub container: String,
pub read_only: bool,
}
#[derive(Debug)]
pub struct Container {
runtime: &'static str,
id: String,
stopped: bool,
}
#[derive(Clone, Debug)]
struct RegisteredContainer {
runtime: &'static str,
id: String,
}
static ACTIVE_CONTAINER: Mutex<Option<RegisteredContainer>> = Mutex::new(None);
static SIGNAL_CLEANUP_INSTALLED: AtomicBool = AtomicBool::new(false);
static SIGNAL_WRITE_FD: AtomicI32 = AtomicI32::new(-1);
pub fn install_signals() -> anyhow::Result<()> {
if SIGNAL_CLEANUP_INSTALLED
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_err()
{
return Ok(());
}
let mut fds = [0; 2];
if unsafe { libc::pipe(fds.as_mut_ptr()) } == -1 {
SIGNAL_CLEANUP_INSTALLED.store(false, Ordering::SeqCst);
return Err(io::Error::last_os_error()).context("creating signal cleanup pipe");
}
set_close_on_exec(fds[0]);
set_close_on_exec(fds[1]);
SIGNAL_WRITE_FD.store(fds[1], Ordering::SeqCst);
thread::spawn(move || signal_cleanup_loop(fds[0]));
install_signal_handler(libc::SIGINT)?;
install_signal_handler(libc::SIGTERM)?;
Ok(())
}
impl Container {
pub fn start(
runtime: &ContainerRuntime,
image: &str,
name: &str,
mounts: &[Mount],
) -> anyhow::Result<Self> {
let runtime_str = runtime.as_str();
let mut cmd = Command::new(runtime_str);
cmd.arg("run")
.arg("-d")
.arg("--rm")
.arg("--name")
.arg(name)
.arg("--read-only")
.arg("--tmpfs")
.arg("/tmp")
.arg("--tmpfs")
.arg("/dest")
.arg("--tmpfs")
.arg("/sysroot")
.arg("--network=none");
if matches!(runtime, ContainerRuntime::Podman) {
cmd.arg("--userns=keep-id");
}
for mount in mounts {
let mut spec = format!("{}:{}", mount.host.display(), mount.container);
if mount.read_only {
spec.push_str(":ro");
}
cmd.arg("-v").arg(spec);
}
cmd.arg(image).arg("sleep").arg("infinity");
cmd.stdout(Stdio::piped()).stderr(Stdio::inherit());
let output = cmd
.output()
.with_context(|| format!("spawning `{runtime_str} run` for image `{image}`"))?;
if !output.status.success() {
bail!(
"`{runtime_str} run` failed with {} for image `{image}`",
output.status
);
}
let id = String::from_utf8(output.stdout)
.context("container id is not valid UTF-8")?
.trim()
.to_owned();
if id.is_empty() {
bail!("`{runtime_str} run` returned an empty container id");
}
register_container(runtime_str, &id);
Ok(Self {
runtime: runtime_str,
id,
stopped: false,
})
}
pub fn exec(&self, argv: &[String], env: &[(String, String)], cwd: &str) -> anyhow::Result<()> {
if argv.is_empty() {
bail!("ctx.run called with an empty argv");
}
let mut cmd = Command::new(self.runtime);
cmd.arg("exec").arg("-w").arg(cwd);
for (k, v) in env {
cmd.arg("-e").arg(format!("{k}={v}"));
}
cmd.arg(&self.id);
cmd.args(argv);
let status = cmd.status().with_context(|| {
format!("spawning `{} exec` in container {}", self.runtime, self.id)
})?;
if !status.success() {
bail!("command {argv:?} failed with {status}");
}
Ok(())
}
pub fn cp_out(&self, src_in_container: &str, host_dst: &Path) -> anyhow::Result<()> {
let spec = format!("{}:{}", self.id, src_in_container);
let status = Command::new(self.runtime)
.arg("cp")
.arg(spec)
.arg(host_dst)
.status()
.with_context(|| format!("spawning `{} cp`", self.runtime))?;
if !status.success() {
bail!(
"`{} cp` failed with {status} for {src_in_container} -> {}",
self.runtime,
host_dst.display()
);
}
Ok(())
}
}
impl Drop for Container {
fn drop(&mut self) {
if !self.stopped {
let _ = Command::new(self.runtime)
.arg("rm")
.arg("-f")
.arg(&self.id)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status();
self.stopped = true;
}
unregister_container(&self.id);
}
}
fn register_container(runtime: &'static str, id: &str) {
if let Ok(mut active) = ACTIVE_CONTAINER.lock() {
*active = Some(RegisteredContainer {
runtime,
id: id.to_owned(),
});
}
}
fn unregister_container(id: &str) {
if let Ok(mut active) = ACTIVE_CONTAINER.lock()
&& active.as_ref().is_some_and(|container| container.id == id)
{
*active = None;
}
}
fn active_container() -> Option<RegisteredContainer> {
ACTIVE_CONTAINER
.lock()
.ok()
.and_then(|active| active.clone())
}
fn signal_cleanup_loop(read_fd: RawFd) {
let mut signals = unsafe { File::from_raw_fd(read_fd) };
let mut buffer = [0_u8; 16];
loop {
match signals.read(&mut buffer) {
Ok(0) => return,
Ok(len) => {
let signal = buffer[..len]
.iter()
.copied()
.find(|signal| *signal != 0)
.unwrap_or(libc::SIGINT as u8);
cleanup_after_signal(signal as i32);
}
Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
Err(_) => return,
}
}
}
fn cleanup_after_signal(signal: i32) -> ! {
if let Some(container) = active_container() {
eprintln!(
"\nreceived signal {signal}; killing container {}",
container.id
);
kill_container_detached(container.runtime, &container.id);
}
std::process::exit(128 + signal);
}
fn kill_container_detached(runtime: &str, id: &str) {
let _ = Command::new(runtime)
.arg("kill")
.arg(id)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn();
}
fn set_close_on_exec(fd: RawFd) {
unsafe {
let flags = libc::fcntl(fd, libc::F_GETFD);
if flags != -1 {
let _ = libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC);
}
}
}
fn install_signal_handler(signal: i32) -> anyhow::Result<()> {
let mut action = unsafe { std::mem::zeroed::<libc::sigaction>() };
action.sa_sigaction = handle_signal as *const () as usize;
action.sa_flags = 0;
unsafe {
libc::sigemptyset(&mut action.sa_mask);
if libc::sigaction(signal, &action, ptr::null_mut()) == -1 {
return Err(io::Error::last_os_error())
.with_context(|| format!("installing handler for signal {signal}"));
}
}
Ok(())
}
extern "C" fn handle_signal(signal: libc::c_int) {
let fd = SIGNAL_WRITE_FD.load(Ordering::Relaxed);
if fd < 0 {
return;
}
let signal = signal as u8;
unsafe {
libc::write(fd, ptr::addr_of!(signal).cast(), 1);
}
}
-99
View File
@@ -1,99 +0,0 @@
use std::{
collections::HashMap,
path::Path,
sync::{Arc, Mutex},
};
mod podman;
pub use podman::PodmanRuntime;
#[derive(Clone)]
pub struct Container {
id: String,
runtime: Arc<dyn ContainerRuntime>,
}
impl Container {
fn new(id: String, runtime: Arc<dyn ContainerRuntime>) -> Self {
Self { id, runtime }
}
pub fn id(&self) -> &str {
&self.id
}
pub fn exec<'a, 'e, 'c>(
&self,
argv: impl Into<Vec<&'a str>>,
env: impl Into<Vec<(&'e str, &'e str)>>,
cwd: impl Into<&'c Path>,
) -> anyhow::Result<()> {
self.runtime
.exec(self.id(), argv.into(), env.into(), cwd.into())
}
}
pub trait ContainerRuntime {
/// Starts a new container, returns the ID.
fn start_container(
&self,
image_name: &str,
mounts: &[(&Path, &str, bool)],
) -> anyhow::Result<String>;
/// Stops a container.
fn stop_container(&self, container_id: &str);
/// Executes a command in a container.
fn exec(
&self,
container_id: &str,
argv: Vec<&str>,
env: Vec<(&str, &str)>,
cwd: &Path,
) -> anyhow::Result<()>;
}
pub struct ContainerManager {
inner: Mutex<ContainerManagerInner>,
}
struct ContainerManagerInner {
containers: HashMap<String, Container>,
runtime: Arc<dyn ContainerRuntime>,
}
impl ContainerManager {
pub fn new(runtime: Arc<dyn ContainerRuntime>) -> Self {
Self {
inner: Mutex::new(ContainerManagerInner {
containers: HashMap::new(),
runtime,
}),
}
}
pub fn container(&self, name: &str) -> anyhow::Result<Container> {
let mut inner = self.inner.lock().unwrap();
if inner.containers.get(name).is_none() {
let container_id = inner.runtime.start_container("alpine:edge", &[])?;
crate::log!("info", "Started new container ({container_id})");
let container = Container::new(container_id, inner.runtime.clone());
inner.containers.insert(name.into(), container);
}
Ok(inner.containers.get(name).cloned().unwrap())
}
}
impl Drop for ContainerManager {
fn drop(&mut self) {
let inner = self.inner.lock().unwrap();
for (_, container) in inner.containers.iter() {
inner.runtime.stop_container(container.id());
}
}
}
-101
View File
@@ -1,101 +0,0 @@
use crate::container::ContainerRuntime;
use anyhow::Context;
use std::{
path::Path,
process::{Command, Stdio},
};
pub struct PodmanRuntime;
impl PodmanRuntime {
pub fn new() -> anyhow::Result<Self> {
let output = Command::new("podman").arg("--version").output()?;
if output.status.success() {
Ok(Self)
} else {
anyhow::bail!(
"Could not execute `podman --version` - make sure you have podman installed."
);
}
}
}
impl ContainerRuntime for PodmanRuntime {
fn start_container(
&self,
image_name: &str,
mounts: &[(&Path, &str, bool)],
) -> anyhow::Result<String> {
let mut cmd = Command::new("podman");
cmd.arg("run");
cmd.arg("--detach");
cmd.arg("--read-only");
cmd.arg("--network=none");
cmd.arg("--userns=keep-id");
cmd.arg("--tmpfs").arg("/builder/dest");
cmd.arg("--tmpfs").arg("/builder/sysroot");
for &(host, container, read_only) in mounts {
cmd.arg("--volume").arg(format!(
"{}:{}{}",
host.display(),
container,
if read_only { ":ro" } else { "" }
));
}
cmd.arg(image_name);
cmd.arg("sleep").arg("infinity");
let output = cmd.output()?;
if output.status.success() {
Ok(String::from_utf8(output.stdout)
.context("container ID is not valid UTF-8")?
.trim()
.into())
} else {
todo!()
}
}
fn stop_container(&self, container_id: &str) {
Command::new("podman")
.arg("kill")
.arg(container_id)
.output()
.unwrap();
}
fn exec(
&self,
container_id: &str,
argv: Vec<&str>,
env: Vec<(&str, &str)>,
cwd: &Path,
) -> anyhow::Result<()> {
let mut cmd = Command::new("podman");
cmd.arg("exec");
cmd.arg("--workdir").arg(cwd);
for (key, value) in env {
cmd.arg("--env").arg(format!("{key}={value}"));
}
cmd.arg(container_id);
cmd.args(argv);
cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
let output = cmd.output()?;
if output.status.success() {
Ok(())
} else {
anyhow::bail!("Failed to execute command");
}
}
}
+193
View File
@@ -0,0 +1,193 @@
use anyhow::Context;
use starlark::{
environment::{FrozenModule, Globals, GlobalsBuilder, Module},
eval::Evaluator,
syntax::{AstModule, Dialect},
values::list::{ListRef, UnpackList},
};
use std::path::{Path, PathBuf};
use walkdir::WalkDir;
use crate::{
options::Options,
phase::Path as StarPath,
recipe::{GitSource, Metadata, Source, Subpackage, TarballSource},
};
#[derive(Debug)]
pub enum ExtractError {
NotFound,
TypeMismatch,
}
impl std::fmt::Display for ExtractError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ExtractError::NotFound => write!(f, "missing"),
ExtractError::TypeMismatch => write!(f, "wrong type"),
}
}
}
impl std::error::Error for ExtractError {}
#[starlark::starlark_module]
fn builder_globals(builder: &mut GlobalsBuilder) {
fn meta(
maintainer: Option<String>,
description: Option<String>,
license: Option<String>,
website: Option<String>,
) -> anyhow::Result<Metadata> {
Ok(Metadata::new(maintainer, description, license, website))
}
fn tarball_source(
url: String,
sha256: String,
strip_components: Option<u32>,
patches: Option<UnpackList<String>>,
) -> anyhow::Result<Source> {
Ok(Source::Tarball(TarballSource::new(
url,
sha256,
strip_components.unwrap_or(0),
patches.map(|p| p.items).unwrap_or_default(),
)))
}
fn git_source(
url: String,
commit: String,
patches: Option<UnpackList<String>>,
) -> anyhow::Result<Source> {
Ok(Source::Git(GitSource::new(
url,
commit,
patches.map(|p| p.items).unwrap_or_default(),
)))
}
fn subpackage(
name: String,
metadata: &Metadata,
files: UnpackList<String>,
) -> anyhow::Result<Subpackage> {
Ok(Subpackage::new(name, files.items, metadata.clone()))
}
fn path(value: String) -> anyhow::Result<StarPath> {
Ok(StarPath::new(value))
}
}
pub fn eval_file(
path: &Path,
options: Option<&Options>,
lib: Option<&FrozenModule>,
) -> anyhow::Result<Module> {
let module = Module::new();
if let Some(lib) = lib {
module.import_public_symbols(lib);
}
if let Some(options) = options {
inject_options(&module, options);
}
let ast = AstModule::parse_file(path, &dialect()).map_err(|err| anyhow::anyhow!("{err}"))?;
let globals = globals();
let mut eval = Evaluator::new(&module);
eval.eval_module(ast, &globals)
.map_err(|err| anyhow::anyhow!("{err}"))?;
drop(eval);
Ok(module)
}
/// Parse and evaluate every `.star` file under `dir` into a single frozen
/// module whose public bindings can be imported into recipe modules. Returns
/// `Ok(None)` if `dir` doesn't exist or contains no `.star` files.
pub fn eval_lib(dir: &Path, options: Option<&Options>) -> anyhow::Result<Option<FrozenModule>> {
if !dir.exists() {
return Ok(None);
}
let mut files: Vec<PathBuf> = Vec::new();
for entry in WalkDir::new(dir) {
let entry = entry.with_context(|| format!("walking lib directory {}", dir.display()))?;
let path = entry.path();
if entry.file_type().is_file() && path.extension().is_some_and(|ext| ext == "star") {
files.push(path.to_path_buf());
}
}
if files.is_empty() {
return Ok(None);
}
// Sorted for deterministic ordering when later definitions shadow earlier ones.
files.sort();
let module = Module::new();
if let Some(options) = options {
inject_options(&module, options);
}
let dialect = dialect();
let globals = globals();
for file in &files {
let ast = AstModule::parse_file(file, &dialect)
.map_err(|err| anyhow::anyhow!("parsing {}: {err}", file.display()))?;
let mut eval = Evaluator::new(&module);
eval.eval_module(ast, &globals)
.map_err(|err| anyhow::anyhow!("evaluating {}: {err}", file.display()))?;
}
Ok(Some(module.freeze()?))
}
fn dialect() -> Dialect {
Dialect {
enable_top_level_stmt: true,
enable_f_strings: true,
..Dialect::Standard
}
}
fn globals() -> Globals {
GlobalsBuilder::standard().with(builder_globals).build()
}
fn inject_options(module: &Module, options: &Options) {
let value = module.heap().alloc(options.clone());
module.set("options", value);
}
pub fn extract_string(module: &Module, key: &str) -> Result<String, ExtractError> {
module
.get(key)
.ok_or_else(|| ExtractError::NotFound)
.and_then(|v| {
v.unpack_str()
.map(|v| v.to_string())
.ok_or_else(|| ExtractError::TypeMismatch)
})
}
pub fn extract_i32(module: &Module, key: &str) -> Result<i32, ExtractError> {
module
.get(key)
.ok_or(ExtractError::NotFound)
.and_then(|v| v.unpack_i32().ok_or(ExtractError::TypeMismatch))
}
pub fn extract_string_list(module: &Module, key: &str) -> Result<Vec<String>, ExtractError> {
let value = module.get(key).ok_or(ExtractError::NotFound)?;
let list = ListRef::from_value(value).ok_or(ExtractError::TypeMismatch)?;
list.iter()
.map(|v| {
v.unpack_str()
.map(|s| s.to_string())
.ok_or(ExtractError::TypeMismatch)
})
.collect()
}
-154
View File
@@ -1,154 +0,0 @@
use crate::eval::Path;
use allocative::Allocative;
use starlark::{
collections::SmallMap,
environment::GlobalsBuilder,
eval::Evaluator,
starlark_module, starlark_simple_value,
values::{StarlarkValue, Value, ValueLike, none::NoneType},
};
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
use std::{
cell::Cell,
collections::HashMap,
path::{Path as StdPath, PathBuf},
};
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub struct PodmanConfig {
image: String,
dockerfile: PathBuf,
}
impl std::fmt::Display for PodmanConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "podman_config")
}
}
starlark_simple_value!(PodmanConfig);
#[starlark_value(type = "podman_config")]
impl<'v> StarlarkValue<'v> for PodmanConfig {}
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub enum ContainerConfig {
Podman(PodmanConfig),
}
impl std::fmt::Display for ContainerConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "container_config")
}
}
starlark_simple_value!(ContainerConfig);
#[starlark_value(type = "container_config")]
impl<'v> StarlarkValue<'v> for ContainerConfig {}
#[derive(Debug, Clone, Allocative, ProvidesStaticType)]
pub enum ConfigValue {
String(String),
Integer(i32),
Bool(bool),
Path(Path),
}
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub struct Config {
arch: String,
container: ContainerConfig,
recipes_dir: Path,
host_recipes_dir: Path,
options: HashMap<String, ConfigValue>,
}
impl Config {
pub fn arch(&self) -> &str {
&self.arch
}
pub fn container(&self) -> &ContainerConfig {
&self.container
}
pub fn recipes_dir(&self) -> &StdPath {
&self.recipes_dir.path()
}
pub fn host_recipes_dir(&self) -> &StdPath {
&self.host_recipes_dir.path()
}
pub fn options(&self) -> &HashMap<String, ConfigValue> {
&self.options
}
}
impl std::fmt::Display for Config {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "container_config")
}
}
starlark_simple_value!(Config);
#[starlark_value(type = "config")]
impl<'v> StarlarkValue<'v> for Config {}
#[starlark_module]
pub fn config_globals(b: &mut GlobalsBuilder) {
fn podman(
#[starlark(require = named)] image: &str,
#[starlark(require = named)] dockerfile: &Path,
) -> anyhow::Result<ContainerConfig> {
Ok(ContainerConfig::Podman(PodmanConfig {
image: image.to_string(),
dockerfile: dockerfile.path().to_owned(),
}))
}
fn config(
#[starlark(require = named)] arch: &str,
#[starlark(require = named)] container: &ContainerConfig,
#[starlark(require = named)] recipes_dir: &Path,
#[starlark(require = named)] host_recipes_dir: &Path,
#[starlark(kwargs)] kwargs: SmallMap<&str, Value>,
eval: &mut Evaluator,
) -> anyhow::Result<NoneType> {
let config = eval
.extra
.and_then(|extra| extra.downcast_ref::<Cell<Option<Config>>>())
.ok_or_else(|| anyhow::anyhow!("`config` called outside of config.star"))?;
config.set(Some(Config {
arch: arch.to_string(),
container: container.clone(),
recipes_dir: recipes_dir.clone(),
host_recipes_dir: host_recipes_dir.clone(),
options: kwargs
.iter()
.map(|(&k, v)| {
let value = if let Some(str) = v.unpack_str() {
ConfigValue::String(str.to_string())
} else if let Some(num) = v.unpack_i32() {
ConfigValue::Integer(num)
} else if let Some(bool) = v.unpack_bool() {
ConfigValue::Bool(bool)
} else if let Some(path) = v.downcast_ref::<Path>() {
ConfigValue::Path(path.clone())
} else {
anyhow::bail!("config option must be a `string`, `int`, `bool` or `path`");
};
Ok((k.to_string(), value))
})
.collect::<Result<_, _>>()?,
}));
Ok(NoneType)
}
}
-96
View File
@@ -1,96 +0,0 @@
use starlark::{
any::AnyLifetime,
environment::{FrozenModule, Globals, Module},
eval::Evaluator,
syntax::{AstModule, Dialect, DialectTypes},
values::{UnpackValue, Value, type_repr::StarlarkTypeRepr},
};
use std::path::Path as StdPath;
mod config;
mod recipe;
mod types;
#[allow(unused_imports)]
pub use config::*;
#[allow(unused_imports)]
pub use recipe::*;
#[allow(unused_imports)]
pub use types::*;
pub trait UnpackCloned: Sized + StarlarkTypeRepr {
fn unpack_cloned(value: Value<'_>) -> Option<Self>;
}
impl<T> UnpackCloned for T
where
for<'v> T: UnpackValue<'v>,
{
fn unpack_cloned(value: Value<'_>) -> Option<Self> {
T::unpack_value(value).unwrap()
}
}
pub fn eval_files(
path: &[&StdPath],
globals: &Globals,
lib_module: Option<&FrozenModule>,
config: Option<&Config>,
extra: Option<&dyn AnyLifetime>,
) -> anyhow::Result<Module> {
use anyhow::Context;
let module = Module::new();
if let Some(lib_module) = lib_module {
module.import_public_symbols(lib_module);
}
if let Some(config) = config {
module.set("options", module.heap().alloc(config.clone()));
}
let mut paths = path.to_vec();
paths.sort();
let ast_modules = paths
.iter()
.map(|&path| {
let module = AstModule::parse_file(path, &default_dialect())
.map_err(|err| anyhow::anyhow!("{err}"))
.context(format!("parsing file {:?}", path.display()))?;
Ok((path, module))
})
.collect::<anyhow::Result<Vec<(&StdPath, AstModule)>>>()?;
for (path, ast) in ast_modules {
let mut eval = Evaluator::new(&module);
if let Some(extra) = extra {
eval.extra = Some(extra);
}
eval.eval_module(ast, globals)
.map_err(|err| anyhow::anyhow!("{err}"))
.context(format!("evaluating file {:?}", path.display()))?;
}
Ok(module)
}
fn default_dialect() -> Dialect {
Dialect {
enable_def: true,
enable_lambda: true,
enable_load: false,
enable_keyword_only_arguments: false,
enable_positional_only_arguments: false,
enable_types: DialectTypes::Disable,
enable_load_reexport: false,
enable_top_level_stmt: true,
enable_f_strings: true,
..Dialect::Standard
}
}
-202
View File
@@ -1,202 +0,0 @@
use std::cell::Cell;
use allocative::Allocative;
use starlark::{
environment::{GlobalsBuilder, Methods, MethodsBuilder, MethodsStatic},
eval::Evaluator,
starlark_module, starlark_simple_value,
typing::Ty,
values::{
Heap, StarlarkValue, UnpackValue, Value, ValueLike, none::NoneType, tuple::UnpackTuple,
type_repr::StarlarkTypeRepr,
},
};
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
use crate::{
container::{Container, ContainerManager},
eval::{Path, UnpackCloned},
log,
recipe::Source,
};
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub struct TarballSource {
url: String,
sha256: String,
strip_components: u32,
}
impl std::fmt::Display for TarballSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "tarball")
}
}
starlark_simple_value!(TarballSource);
#[starlark_value(type = "tarball")]
impl<'v> StarlarkValue<'v> for TarballSource {}
impl UnpackCloned for TarballSource {
fn unpack_cloned(value: Value<'_>) -> Option<Self> {
value.downcast_ref().cloned()
}
}
impl Source for TarballSource {}
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub struct Metadata {
maintainer: Option<String>,
description: Option<String>,
license: Option<String>,
website: Option<String>,
}
impl std::fmt::Display for Metadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "metadata")
}
}
starlark_simple_value!(Metadata);
#[starlark_value(type = "metadata")]
impl<'v> StarlarkValue<'v> for Metadata {}
impl UnpackCloned for Metadata {
fn unpack_cloned(value: Value<'_>) -> Option<Self> {
value.downcast_ref().cloned()
}
}
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub struct Context {
pub source_dir: Path,
pub build_dir: Path,
pub jobs: i32,
}
impl std::fmt::Display for Context {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "context")
}
}
starlark_simple_value!(Context);
#[derive(Debug)]
struct RunArg(pub String);
impl UnpackValue<'_> for RunArg {
type Error = anyhow::Error;
fn unpack_value_impl(value: Value) -> anyhow::Result<Option<Self>> {
Ok(if let Some(str) = value.unpack_str() {
Some(RunArg(str.to_owned()))
} else if let Some(int) = value.unpack_i32() {
Some(RunArg(int.to_string()))
} else if let Some(path) = value.downcast_ref::<Path>() {
Some(RunArg(path.path().to_str().unwrap_or("").to_string()))
} else {
None
})
}
}
impl StarlarkTypeRepr for RunArg {
type Canonical = Self;
fn starlark_type_repr() -> starlark::typing::Ty {
Ty::string()
}
}
#[derive(ProvidesStaticType)]
pub struct ContainerManagerWrapper<'a>(pub &'a ContainerManager);
#[starlark_module]
fn context_methods(b: &mut MethodsBuilder) {
fn run(
#[starlark(this)] this: &Context,
#[starlark(args)] args: UnpackTuple<RunArg>,
eval: &mut Evaluator,
) -> anyhow::Result<NoneType> {
let ContainerManagerWrapper(container_manager) = eval
.extra
.and_then(|extra| extra.downcast_ref())
.ok_or_else(|| anyhow::anyhow!("`config` called outside of config.star"))?;
let argv = args.items.iter().map(|x| x.0.as_str()).collect::<Vec<_>>();
log!("run", "Running command: {argv:?}");
container_manager
.container("changeme")? // TODO
.exec(argv, [], std::path::Path::new("/"))?;
Ok(NoneType)
}
}
#[starlark_value(type = "context")]
impl<'v> StarlarkValue<'v> for Context {
fn get_methods() -> Option<&'static Methods> {
static RES: MethodsStatic = MethodsStatic::new();
RES.methods(context_methods)
}
fn has_attr(&self, attr: &str, _heap: &Heap) -> bool {
match attr {
"source_dir" => true,
"build_dir" => true,
"jobs" => true,
_ => false,
}
}
fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option<Value<'v>> {
match attr {
"source_dir" => Some(heap.alloc(self.source_dir.clone())),
"build_dir" => Some(heap.alloc(self.build_dir.clone())),
"jobs" => Some(heap.alloc(self.jobs)),
_ => None,
}
}
}
impl UnpackCloned for Context {
fn unpack_cloned(value: Value<'_>) -> Option<Self> {
value.downcast_ref().cloned()
}
}
#[starlark_module]
pub fn recipe_globals(b: &mut GlobalsBuilder) {
fn tarball(
#[starlark(require = named)] url: &str,
#[starlark(require = named)] sha256: &str,
#[starlark(require = named, default = 0)] strip_components: u32,
) -> anyhow::Result<TarballSource> {
Ok(TarballSource {
url: url.to_string(),
sha256: sha256.to_string(),
strip_components,
})
}
fn meta(
#[starlark(require = named)] maintainer: Option<&str>,
#[starlark(require = named)] description: Option<&str>,
#[starlark(require = named)] license: Option<&str>,
#[starlark(require = named)] website: Option<&str>,
) -> anyhow::Result<Metadata> {
Ok(Metadata {
maintainer: maintainer.map(|x| x.to_string()),
description: description.map(|x| x.to_string()),
license: license.map(|x| x.to_string()),
website: website.map(|x| x.to_string()),
})
}
}
-64
View File
@@ -1,64 +0,0 @@
use allocative::Allocative;
use starlark::{
environment::GlobalsBuilder,
starlark_module, starlark_simple_value,
values::{Heap, StarlarkValue, Value, ValueLike},
};
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
use std::path::{Path as StdPath, PathBuf};
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
pub struct Path {
inner: PathBuf,
}
impl Path {
pub fn new(value: impl Into<PathBuf>) -> Self {
Self {
inner: value.into(),
}
}
pub fn path(&self) -> &StdPath {
&self.inner
}
}
impl std::fmt::Display for Path {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "path({:?})", self.inner)
}
}
starlark_simple_value!(Path);
#[starlark_value(type = "path")]
impl<'v> StarlarkValue<'v> for Path {
fn div(&self, other: Value<'v>, heap: &'v Heap) -> starlark::Result<Value<'v>> {
let rhs = if let Some(str) = other.unpack_str() {
str.to_string()
} else if let Some(path) = other.downcast_ref::<Path>() {
path.inner.to_str().unwrap_or("").to_string()
} else {
return Err(starlark::Error::new_other(anyhow::anyhow!(
"expected a `string` or `path`, got `{}`",
other.get_type()
)));
};
Ok(heap.alloc(Path {
inner: self
.inner
.join(rhs.trim_start_matches(std::path::MAIN_SEPARATOR_STR)),
}))
}
}
#[starlark_module]
pub fn types_globals(b: &mut GlobalsBuilder) {
fn path(value: &str) -> anyhow::Result<Path> {
Ok(Path {
inner: PathBuf::from(value),
})
}
}
+434
View File
@@ -0,0 +1,434 @@
use std::{
collections::{BTreeMap, BTreeSet},
fmt, fs,
path::Path,
};
use anyhow::bail;
use crate::{
layout::Layout,
recipe::{OutputPackage, Recipe, RecipeKind, RecipeSet},
};
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum TaskId {
FetchSources(String),
PrepareSources(String),
PrepareRecipe(String),
ConfigureRecipe(String),
BuildRecipe(String),
InstallPackageFiles(String),
ProduceApk(String),
InstallHostRecipe(String),
}
impl fmt::Display for TaskId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::FetchSources(recipe) => write!(f, "fetch sources {recipe}"),
Self::PrepareSources(recipe) => write!(f, "prepare sources {recipe}"),
Self::PrepareRecipe(recipe) => write!(f, "prepare recipe {recipe}"),
Self::ConfigureRecipe(recipe) => write!(f, "configure {recipe}"),
Self::BuildRecipe(recipe) => write!(f, "build {recipe}"),
Self::InstallPackageFiles(output) => write!(f, "install package files {output}"),
Self::ProduceApk(output) => write!(f, "produce apk {output}"),
Self::InstallHostRecipe(recipe) => write!(f, "install host recipe {recipe}"),
}
}
}
#[derive(Debug)]
pub struct TaskPlan {
dependencies: BTreeMap<TaskId, Vec<TaskId>>,
order: Vec<TaskId>,
}
impl TaskPlan {
pub fn order(&self) -> &[TaskId] {
&self.order
}
pub fn is_empty(&self) -> bool {
self.order.is_empty()
}
pub fn dependency_count(&self) -> usize {
self.dependencies.values().map(Vec::len).sum()
}
}
pub struct TaskPlanner<'a> {
layout: Layout<'a>,
recipes: &'a RecipeSet,
forced_recipes: BTreeSet<String>,
dependencies: BTreeMap<TaskId, Vec<TaskId>>,
inactive: BTreeSet<TaskId>,
visiting: BTreeSet<TaskId>,
visited: BTreeSet<TaskId>,
}
impl<'a> TaskPlanner<'a> {
pub fn new(root: &'a Path, arch: &'a str, recipes: &'a RecipeSet) -> Self {
Self {
layout: Layout::new(root, arch),
recipes,
forced_recipes: BTreeSet::new(),
dependencies: BTreeMap::new(),
inactive: BTreeSet::new(),
visiting: BTreeSet::new(),
visited: BTreeSet::new(),
}
}
pub fn build_plan(mut self, requests: &[String], force: bool) -> anyhow::Result<TaskPlan> {
for request in requests {
let recipe = self.recipes.recipe(request)?;
if force {
self.forced_recipes.insert(recipe.key());
}
match recipe.kind() {
RecipeKind::Package => {
for output in recipe.outputs() {
self.visit(TaskId::ProduceApk(output.key()))?;
}
}
RecipeKind::HostPackage => {
self.visit(TaskId::InstallHostRecipe(recipe.key()))?;
}
}
}
self.into_plan()
}
pub fn fetch_plan(mut self, requests: &[String]) -> anyhow::Result<TaskPlan> {
for request in requests {
let recipe = self.recipes.recipe(request)?;
self.visit(TaskId::FetchSources(recipe.key()))?;
}
self.into_plan()
}
fn visit(&mut self, task: TaskId) -> anyhow::Result<()> {
if self.visited.contains(&task) || self.inactive.contains(&task) {
return Ok(());
}
if !self.is_active(&task)? {
self.inactive.insert(task);
return Ok(());
}
if !self.visiting.insert(task.clone()) {
bail!("task dependency cycle involving `{task}`");
}
let dependencies = self.dependencies(&task)?;
let mut active_dependencies = Vec::new();
for dependency in dependencies {
self.visit(dependency.clone())?;
if self.dependencies.contains_key(&dependency) {
active_dependencies.push(dependency);
}
}
self.visiting.remove(&task);
self.visited.insert(task.clone());
self.dependencies.insert(task, active_dependencies);
Ok(())
}
fn into_plan(self) -> anyhow::Result<TaskPlan> {
let order = recipe_contiguous_order(&self.dependencies, self.recipes)?;
Ok(TaskPlan {
dependencies: self.dependencies,
order,
})
}
fn dependencies(&self, task: &TaskId) -> anyhow::Result<Vec<TaskId>> {
match task {
TaskId::FetchSources(_) => Ok(Vec::new()),
TaskId::PrepareSources(recipe) => Ok(vec![TaskId::FetchSources(recipe.clone())]),
TaskId::PrepareRecipe(recipe) => Ok(vec![TaskId::PrepareSources(recipe.clone())]),
TaskId::ConfigureRecipe(recipe) => {
let recipe = self.recipes.recipe(recipe)?;
let mut deps = vec![
TaskId::PrepareSources(recipe.key()),
TaskId::PrepareRecipe(recipe.key()),
];
deps.extend(
recipe
.host_deps()
.iter()
.map(|dep| TaskId::InstallHostRecipe(RecipeKind::HostPackage.key(dep))),
);
deps.extend(
recipe
.build_deps()
.iter()
.chain(recipe.deps().iter())
.map(|dep| TaskId::ProduceApk(dep.clone())),
);
Ok(deps)
}
TaskId::BuildRecipe(recipe) => Ok(vec![TaskId::ConfigureRecipe(recipe.clone())]),
TaskId::InstallPackageFiles(output) => {
let output = self.recipes.output(output)?;
let recipe = self.recipes.recipe(output.recipe())?;
if output.is_base() {
Ok(vec![TaskId::BuildRecipe(output.recipe().to_owned())])
} else {
let base = recipe.base_output().ok_or_else(|| {
anyhow::anyhow!("recipe `{}` has no base output", recipe.key())
})?;
Ok(vec![TaskId::InstallPackageFiles(base.key())])
}
}
TaskId::ProduceApk(output) => {
let output = self.recipes.output(output)?;
let recipe = self.recipes.recipe(output.recipe())?;
let mut deps = vec![TaskId::InstallPackageFiles(output.key())];
if output.is_base() {
deps.extend(
recipe
.outputs()
.iter()
.filter(|candidate| !candidate.is_base())
.map(|subpackage| TaskId::InstallPackageFiles(subpackage.key())),
);
}
deps.extend(
recipe
.deps()
.iter()
.chain(recipe.run_deps().iter())
.map(|dep| TaskId::ProduceApk(dep.clone())),
);
Ok(deps)
}
TaskId::InstallHostRecipe(recipe) => {
self.recipes.recipe(recipe)?;
Ok(vec![TaskId::BuildRecipe(recipe.clone())])
}
}
}
fn is_active(&self, task: &TaskId) -> anyhow::Result<bool> {
match task {
TaskId::FetchSources(recipe) => self.fetch_sources_active(self.recipes.recipe(recipe)?),
TaskId::PrepareSources(recipe) => {
self.prepare_sources_active(self.recipes.recipe(recipe)?)
}
TaskId::PrepareRecipe(recipe) => {
self.prepare_recipe_active(self.recipes.recipe(recipe)?)
}
TaskId::ConfigureRecipe(recipe) => {
self.recipe_task_active(self.recipes.recipe(recipe)?, "configure")
}
TaskId::BuildRecipe(recipe) => {
self.recipe_task_active(self.recipes.recipe(recipe)?, "build")
}
TaskId::InstallPackageFiles(output) => {
let output = self.recipes.output(output)?;
let recipe = self.recipes.recipe(output.recipe())?;
if output.is_base() {
return recipe.outputs().iter().try_fold(false, |active, output| {
Ok(active
|| self.output_task_active(recipe, output, "install")?
|| self.produce_apk_active(recipe, output)?)
});
}
let base = recipe.base_output().ok_or_else(|| {
anyhow::anyhow!("recipe `{}` has no base output", recipe.key())
})?;
Ok(self.output_task_active(recipe, output, "install")?
|| self.produce_apk_active(recipe, output)?
|| self.produce_apk_active(recipe, base)?)
}
TaskId::ProduceApk(output) => {
let output = self.recipes.output(output)?;
let recipe = self.recipes.recipe(output.recipe())?;
self.produce_apk_active(recipe, output)
}
TaskId::InstallHostRecipe(recipe) => {
let recipe = self.recipes.recipe(recipe)?;
self.install_host_recipe_active(recipe)
}
}
}
fn fetch_sources_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
Ok(recipe.sources().entries().iter().any(|(_, source)| {
source.is_unknown_cache_key()
|| !self.layout.source_cache_path(source.cache_key()).exists()
}))
}
fn is_recipe_forced(&self, recipe: &Recipe) -> bool {
self.forced_recipes.contains(&recipe.key())
}
fn prepare_sources_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
let want_version = format!("{}-r{}", recipe.version(), recipe.revision());
if fs::read_to_string(self.layout.source_stamp(recipe, "version"))
.ok()
.as_deref()
!= Some(want_version.as_str())
{
return Ok(true);
}
if self.layout.recipe_has_patches(recipe)?
&& !self.layout.source_stamp(recipe, "patched").exists()
{
return Ok(true);
}
Ok(false)
}
fn prepare_recipe_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
if recipe.phases().prepare().is_none() {
return Ok(false);
}
if self.prepare_sources_active(recipe)? {
return Ok(true);
}
self.recipe_task_active(recipe, "prepare")
}
fn recipe_task_active(&self, recipe: &Recipe, kind: &str) -> anyhow::Result<bool> {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
Ok(
fs::read_to_string(self.layout.recipe_task_stamp(recipe, kind))
.ok()
.as_deref()
!= Some(self.layout.recipe_fingerprint(recipe)?.as_str()),
)
}
fn output_task_active(
&self,
recipe: &Recipe,
output: &OutputPackage,
kind: &str,
) -> anyhow::Result<bool> {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
Ok(
fs::read_to_string(self.layout.output_task_stamp(output, kind))
.ok()
.as_deref()
!= Some(self.layout.output_fingerprint(recipe, output)?.as_str()),
)
}
fn produce_apk_active(&self, recipe: &Recipe, output: &OutputPackage) -> anyhow::Result<bool> {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
if !self.layout.apk_path(recipe, output).exists() {
return Ok(true);
}
Ok(
fs::read_to_string(self.layout.output_task_stamp(output, "apk"))
.ok()
.as_deref()
!= Some(self.layout.output_fingerprint(recipe, output)?.as_str()),
)
}
fn install_host_recipe_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
if !self.layout.host_install_dir(recipe).exists() {
return Ok(true);
}
Ok(
fs::read_to_string(self.layout.recipe_task_stamp(recipe, "host-install"))
.ok()
.as_deref()
!= Some(self.layout.recipe_fingerprint(recipe)?.as_str()),
)
}
}
fn recipe_contiguous_order(
dependencies: &BTreeMap<TaskId, Vec<TaskId>>,
recipes: &RecipeSet,
) -> anyhow::Result<Vec<TaskId>> {
let total = dependencies.len();
let mut remaining: BTreeMap<TaskId, BTreeSet<TaskId>> = dependencies
.iter()
.map(|(task, deps)| (task.clone(), deps.iter().cloned().collect()))
.collect();
let mut dependents: BTreeMap<TaskId, Vec<TaskId>> = BTreeMap::new();
for (task, deps) in dependencies {
for dep in deps {
dependents
.entry(dep.clone())
.or_default()
.push(task.clone());
}
}
let mut order = Vec::with_capacity(total);
let mut current_recipe: Option<String> = None;
while order.len() < total {
let next = pick_next(&remaining, current_recipe.as_deref(), recipes)?;
let slug = task_recipe_slug(&next, recipes)?;
current_recipe = Some(slug);
remaining.remove(&next);
if let Some(children) = dependents.get(&next) {
for child in children {
if let Some(deps) = remaining.get_mut(child) {
deps.remove(&next);
}
}
}
order.push(next);
}
Ok(order)
}
fn pick_next(
remaining: &BTreeMap<TaskId, BTreeSet<TaskId>>,
current_recipe: Option<&str>,
recipes: &RecipeSet,
) -> anyhow::Result<TaskId> {
let mut fallback: Option<TaskId> = None;
for (task, deps) in remaining {
if !deps.is_empty() {
continue;
}
if let Some(active) = current_recipe {
let slug = task_recipe_slug(task, recipes)?;
if slug == active {
return Ok(task.clone());
}
}
if fallback.is_none() {
fallback = Some(task.clone());
}
}
fallback.ok_or_else(|| anyhow::anyhow!("task dependency cycle detected"))
}
pub fn task_recipe_slug(task: &TaskId, recipes: &RecipeSet) -> anyhow::Result<String> {
Ok(match task {
TaskId::FetchSources(recipe)
| TaskId::PrepareSources(recipe)
| TaskId::PrepareRecipe(recipe)
| TaskId::ConfigureRecipe(recipe)
| TaskId::BuildRecipe(recipe)
| TaskId::InstallHostRecipe(recipe) => recipe.clone(),
TaskId::InstallPackageFiles(output) | TaskId::ProduceApk(output) => {
recipes.output(output)?.recipe().to_owned()
}
})
}
+123
View File
@@ -0,0 +1,123 @@
use std::{
fs,
path::{Path, PathBuf},
};
use anyhow::Context;
use sha2::{Digest, Sha256};
use crate::recipe::{OutputPackage, Recipe};
pub struct Layout<'a> {
pub root: &'a Path,
pub arch: &'a str,
}
impl<'a> Layout<'a> {
pub fn new(root: &'a Path, arch: &'a str) -> Self {
Self { root, arch }
}
pub fn source_cache_dir(&self) -> PathBuf {
self.root.join("build/cache/sources")
}
pub fn source_cache_path(&self, key: &str) -> PathBuf {
self.source_cache_dir().join(key)
}
pub fn source_workdir(&self, recipe: &Recipe) -> PathBuf {
self.root.join("build/sources").join(recipe.slug())
}
pub fn build_workdir(&self, recipe: &Recipe) -> PathBuf {
self.root.join("build/builds").join(recipe.slug())
}
pub fn host_install_dir(&self, recipe: &Recipe) -> PathBuf {
self.root.join("build/host-pkgs").join(recipe.slug())
}
pub fn host_install_root(&self, recipe: &Recipe) -> PathBuf {
self.root.join("build/host-pkgs").join(recipe.slug())
}
pub fn apk_path(&self, recipe: &Recipe, output: &OutputPackage) -> PathBuf {
self.root.join("build/pkgs").join(self.arch).join(format!(
"{}-{}-r{}.apk",
output.name(),
recipe.version(),
recipe.revision()
))
}
pub fn source_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf {
self.root
.join("build/sources")
.join(format!("{}.{kind}", recipe.slug()))
}
pub fn recipe_task_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf {
self.root
.join("build/tasks")
.join(format!("{}.{kind}", recipe.slug()))
}
pub fn output_task_stamp(&self, output: &OutputPackage, kind: &str) -> PathBuf {
self.root
.join("build/tasks")
.join(format!("{}.{kind}", output.key().replace(':', "-")))
}
pub fn recipe_fingerprint(&self, recipe: &Recipe) -> anyhow::Result<String> {
let mut hasher = Sha256::new();
hasher.update(self.arch.as_bytes());
hasher.update(recipe.key().as_bytes());
hasher.update(recipe.version().as_bytes());
hasher.update(recipe.revision().to_le_bytes());
hasher.update(
fs::read(recipe.path())
.with_context(|| format!("reading recipe {}", recipe.path().display()))?,
);
for (name, source) in recipe.sources().entries() {
hasher.update(name.unwrap_or("").as_bytes());
hasher.update(source.url().as_bytes());
hasher.update(source.cache_key().as_bytes());
}
for patch in self.recipe_patches(recipe)? {
hasher.update(patch.display().to_string().as_bytes());
hasher
.update(fs::read(&patch).with_context(|| format!("reading {}", patch.display()))?);
}
Ok(hex::encode(hasher.finalize()))
}
pub fn output_fingerprint(
&self,
recipe: &Recipe,
output: &OutputPackage,
) -> anyhow::Result<String> {
let mut hasher = Sha256::new();
hasher.update(self.recipe_fingerprint(recipe)?.as_bytes());
hasher.update(output.key().as_bytes());
Ok(hex::encode(hasher.finalize()))
}
pub fn recipe_has_patches(&self, recipe: &Recipe) -> anyhow::Result<bool> {
Ok(!self.recipe_patches(recipe)?.is_empty())
}
pub fn recipe_patches(&self, recipe: &Recipe) -> anyhow::Result<Vec<PathBuf>> {
let Some(data_dir) = recipe.data_dir() else {
return Ok(Vec::new());
};
let patches_dir = data_dir.join("patches");
let mut out = Vec::new();
for (_, source) in recipe.sources().entries() {
for name in source.patches() {
out.push(patches_dir.join(name));
}
}
Ok(out)
}
}
+15 -9
View File
@@ -2,19 +2,25 @@ use std::{io::IsTerminal, sync::LazyLock};
static IS_STDERR_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stderr().is_terminal());
pub fn __emit(color: &str, action: &str, args: std::fmt::Arguments) {
const ARROW: &str = "==>";
const ARROW: &str = "==>";
fn emit(color: &str, action: &str, details: &str) {
if *IS_STDERR_TERMINAL {
eprintln!("\x1b[{color}m{ARROW}\x1b[0m \x1b[1m{action}\x1b[0m {args}");
eprintln!("\x1b[{color}m{ARROW} \x1b[1m{action} \x1b[0m{details}");
} else {
eprintln!("{ARROW} {action} {args}");
eprintln!("{ARROW} {action} {details}");
}
}
#[macro_export]
macro_rules! log {
($action:literal, $($arg:tt)*) => {{
$crate::log::__emit("1;34", $action, format_args!($($arg)*));
}};
pub fn step(action: &str, details: &str) {
emit("1;34", action, details);
}
pub fn skip(action: &str, details: &str) {
emit("1;33", action, details);
}
#[allow(dead_code)]
pub fn info(action: &str, details: &str) {
emit("1;32", action, details);
}
+7 -127
View File
@@ -1,136 +1,16 @@
use crate::{
container::{ContainerManager, PodmanRuntime},
plan::{Plan, PlanKey},
recipe::{PackageRecipe, RecipeSet, SourceRecipe, ToolRecipe},
};
use std::{path::Path, sync::Arc};
mod builder;
mod cli;
mod config;
mod container;
mod eval;
mod graph;
mod layout;
mod log;
mod plan;
mod options;
mod phase;
mod recipe;
fn main() -> anyhow::Result<()> {
container::install_signals()?;
cli::run()
// let podman_runtime = Arc::new(PodmanRuntime::new()?);
// let mut container_manager = ContainerManager::new(podman_runtime);
// container_manager.container("example")?.exec(
// vec!["sh", "-c", "uname -a && id"],
// vec![],
// Path::new("/"),
// )?;
// let mut recipes = RecipeSet::default();
// recipes.load_recipes(Path::new("./recipes"), Path::new("./host-recipes"))?;
// recipes.add_source(
// "binutils-2.46",
// SourceRecipe {
// name: "binutils-2.46".into(),
// },
// );
// recipes.add_source(
// "gcc-16.1.0",
// SourceRecipe {
// name: "gcc-16.1.0".into(),
// },
// );
// recipes.add_source(
// "linux-7.0.9",
// SourceRecipe {
// name: "linux-7.0.9".into(),
// },
// );
// recipes.add_source(
// "glibc-2.41",
// SourceRecipe {
// name: "glibc-2.41".into(),
// },
// );
// recipes.add_source(
// "bash-5.3",
// SourceRecipe {
// name: "bash-5.3".into(),
// },
// );
// recipes.add_tool(
// "binutils",
// ToolRecipe {
// name: "binutils".into(),
// sources: vec!["binutils-2.46".into()],
// tools_wanted: vec![],
// pkgs_wanted: vec![],
// },
// );
// recipes.add_tool(
// "gcc-bootstrap",
// ToolRecipe {
// name: "gcc-bootstrap".into(),
// sources: vec!["gcc-16.1.0".into()],
// tools_wanted: vec!["binutils".into()],
// pkgs_wanted: vec![],
// },
// );
// recipes.add_tool(
// "gcc",
// ToolRecipe {
// name: "gcc".into(),
// sources: vec!["gcc-16.1.0".into()],
// tools_wanted: vec!["binutils".into()],
// pkgs_wanted: vec!["glibc".into()],
// },
// );
// recipes.add_package(
// "linux-headers",
// PackageRecipe {
// name: "linux-headers".into(),
// sources: vec!["linux-7.0.9".into()],
// tools_wanted: vec![],
// pkgs_wanted: vec![],
// },
// );
// recipes.add_package(
// "glibc",
// PackageRecipe {
// name: "glibc".into(),
// sources: vec!["glibc-2.41".into()],
// tools_wanted: vec!["gcc-bootstrap".into()],
// pkgs_wanted: vec!["linux-headers".into()],
// },
// );
// recipes.add_package(
// "bash",
// PackageRecipe {
// name: "bash".into(),
// sources: vec!["bash-5.3".into()],
// tools_wanted: vec!["gcc".into()],
// pkgs_wanted: vec!["glibc".into()],
// },
// );
// let mut plan = Plan::new(&recipes);
// plan.add_wanted(PlanKey::PkgPackage(
// recipes.package("bash").expect("back package"),
// ));
// println!("{:#?}", plan.steps()?);
// Ok(())
}
+35
View File
@@ -0,0 +1,35 @@
use allocative::Allocative;
use starlark::values::{Heap, OwnedFrozenValue, StarlarkValue, Value};
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
use std::collections::HashMap;
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub struct Options {
entries: HashMap<String, OwnedFrozenValue>,
}
impl Options {
pub fn new(entries: HashMap<String, OwnedFrozenValue>) -> Self {
Self { entries }
}
}
impl std::fmt::Display for Options {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "options")
}
}
starlark::starlark_simple_value!(Options);
#[starlark_value(type = "options")]
impl<'v> StarlarkValue<'v> for Options {
fn get_attr(&self, attribute: &str, _heap: &'v Heap) -> Option<Value<'v>> {
let owned = self.entries.get(attribute)?;
// SAFETY: `self` is kept alive by the module heap into which it was
// allocated, and `owned` holds an Arc to its source frozen heap. The
// returned Value therefore remains valid for as long as the receiving
// module is alive.
Some(unsafe { owned.unchecked_frozen_value() }.to_value())
}
}
+389
View File
@@ -0,0 +1,389 @@
use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
use allocative::Allocative;
use anyhow::anyhow;
use starlark::{
collections::SmallMap,
environment::{Methods, MethodsBuilder, MethodsStatic, Module},
eval::Evaluator,
values::{
Heap, OwnedFrozenValue, StarlarkValue, Value, ValueLike, list::UnpackList, none::NoneType,
},
};
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_module, starlark_value};
use crate::container::Container;
thread_local! {
static CURRENT: RefCell<Option<PhaseRuntime>> = const { RefCell::new(None) };
}
#[derive(Clone)]
pub struct PhaseRuntime {
pub container: Rc<RefCell<Container>>,
pub base_path: String,
pub base_env: Vec<(String, String)>,
}
pub struct PhaseRuntimeGuard;
impl PhaseRuntimeGuard {
pub fn enter(runtime: PhaseRuntime) -> Self {
CURRENT.with(|cell| {
let prev = cell.borrow_mut().replace(runtime);
assert!(prev.is_none(), "phase runtime already set");
});
Self
}
}
impl Drop for PhaseRuntimeGuard {
fn drop(&mut self) {
CURRENT.with(|cell| {
cell.borrow_mut().take();
});
}
}
fn with_current<R>(f: impl FnOnce(&PhaseRuntime) -> R) -> anyhow::Result<R> {
CURRENT.with(|cell| {
let borrow = cell.borrow();
let runtime = borrow
.as_ref()
.ok_or_else(|| anyhow!("ctx.run called outside of a phase invocation"))?;
Ok(f(runtime))
})
}
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub struct Path {
inner: String,
}
impl Path {
pub fn new(inner: impl Into<String>) -> Self {
Self {
inner: inner.into(),
}
}
pub fn as_str(&self) -> &str {
&self.inner
}
}
fn join_paths(base: &str, rhs: &str) -> String {
let trimmed = base.trim_end_matches('/');
if trimmed.is_empty() {
format!("/{rhs}")
} else {
format!("{trimmed}/{rhs}")
}
}
fn coerce_path_string(value: Value<'_>) -> anyhow::Result<String> {
if let Some(s) = value.unpack_str() {
return Ok(s.to_owned());
}
if let Some(p) = value.downcast_ref::<Path>() {
return Ok(p.inner.clone());
}
if let Some(s) = value.downcast_ref::<SourceDir>() {
return Ok(s.default.clone());
}
anyhow::bail!("expected a string or path, got `{}`", value.get_type())
}
impl std::fmt::Display for Path {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.inner)
}
}
starlark::starlark_simple_value!(Path);
#[starlark_value(type = "path")]
impl<'v> StarlarkValue<'v> for Path {
fn div(&self, other: Value<'v>, heap: &'v Heap) -> starlark::Result<Value<'v>> {
let rhs = coerce_path_string(other).map_err(starlark::Error::new_other)?;
Ok(heap.alloc(Path::new(join_paths(&self.inner, &rhs))))
}
fn add(&self, rhs: Value<'v>, heap: &'v Heap) -> Option<starlark::Result<Value<'v>>> {
let suffix = rhs.unpack_str()?;
Some(Ok(heap.alloc(format!("{}{}", self.inner, suffix))))
}
fn radd(&self, lhs: Value<'v>, heap: &'v Heap) -> Option<starlark::Result<Value<'v>>> {
let prefix = lhs.unpack_str()?;
Some(Ok(heap.alloc(format!("{}{}", prefix, self.inner))))
}
fn equals(&self, other: Value<'v>) -> starlark::Result<bool> {
Ok(other
.downcast_ref::<Self>()
.map(|o| o.inner == self.inner)
.unwrap_or(false))
}
}
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub struct SourceDir {
default: String,
entries: BTreeMap<String, String>,
}
impl SourceDir {
pub fn single(path: impl Into<String>) -> Self {
Self {
default: path.into(),
entries: BTreeMap::new(),
}
}
pub fn named<I, K, V>(entries: I) -> Self
where
I: IntoIterator<Item = (K, V)>,
K: Into<String>,
V: Into<String>,
{
let entries: BTreeMap<String, String> = entries
.into_iter()
.map(|(k, v)| (k.into(), v.into()))
.collect();
let default = entries
.values()
.next()
.cloned()
.unwrap_or_else(|| "/sources".to_owned());
Self { default, entries }
}
}
impl std::fmt::Display for SourceDir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.default)
}
}
starlark::starlark_simple_value!(SourceDir);
#[starlark_value(type = "source_dir")]
impl<'v> StarlarkValue<'v> for SourceDir {
fn at(&self, index: Value<'v>, heap: &'v Heap) -> starlark::Result<Value<'v>> {
let key = index.unpack_str().ok_or_else(|| {
starlark::Error::new_other(anyhow!("source_dir index must be a string"))
})?;
let path = self.entries.get(key).ok_or_else(|| {
starlark::Error::new_other(anyhow!(
"no source named `{key}` (available: {})",
if self.entries.is_empty() {
"<none>".to_owned()
} else {
self.entries
.keys()
.map(String::as_str)
.collect::<Vec<_>>()
.join(", ")
}
))
})?;
Ok(heap.alloc(path.as_str()))
}
fn add(&self, rhs: Value<'v>, heap: &'v Heap) -> Option<starlark::Result<Value<'v>>> {
let suffix = rhs.unpack_str()?;
Some(Ok(heap.alloc(format!("{}{}", self.default, suffix))))
}
fn radd(&self, lhs: Value<'v>, heap: &'v Heap) -> Option<starlark::Result<Value<'v>>> {
let prefix = lhs.unpack_str()?;
Some(Ok(heap.alloc(format!("{}{}", prefix, self.default))))
}
fn div(&self, other: Value<'v>, heap: &'v Heap) -> starlark::Result<Value<'v>> {
let rhs = coerce_path_string(other).map_err(starlark::Error::new_other)?;
Ok(heap.alloc(Path::new(join_paths(&self.default, &rhs))))
}
}
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub struct PhaseContext {
source_dir: SourceDir,
build_dir: String,
sysroot: String,
files: Option<String>,
jobs: i32,
}
impl PhaseContext {
pub fn new(source_dir: SourceDir, jobs: i32, files: Option<String>) -> Self {
Self {
source_dir,
build_dir: "/build".to_owned(),
sysroot: "/sysroot".to_owned(),
files,
jobs,
}
}
}
impl std::fmt::Display for PhaseContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "ctx")
}
}
starlark::starlark_simple_value!(PhaseContext);
#[starlark_value(type = "phase_context")]
impl<'v> StarlarkValue<'v> for PhaseContext {
fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option<Value<'v>> {
Some(match attr {
"source_dir" => heap.alloc(self.source_dir.clone()),
"build_dir" => heap.alloc(Path::new(self.build_dir.clone())),
"sysroot" => heap.alloc(Path::new(self.sysroot.clone())),
"files" => heap.alloc(Path::new(self.files.as_ref()?.clone())),
"jobs" => heap.alloc(self.jobs),
_ => return None,
})
}
fn has_attr(&self, attr: &str, _heap: &'v Heap) -> bool {
match attr {
"source_dir" | "build_dir" | "sysroot" | "jobs" | "run" => true,
"files" => self.files.is_some(),
_ => false,
}
}
fn dir_attr(&self) -> Vec<String> {
let mut attrs = vec![
"source_dir".to_owned(),
"build_dir".to_owned(),
"sysroot".to_owned(),
"jobs".to_owned(),
"run".to_owned(),
];
if self.files.is_some() {
attrs.push("files".to_owned());
}
attrs
}
fn get_methods() -> Option<&'static Methods> {
static RES: MethodsStatic = MethodsStatic::new();
RES.methods(phase_context_methods)
}
}
#[starlark_module]
fn phase_context_methods(builder: &mut MethodsBuilder) {
fn run<'v>(
#[starlark(this)] _this: Value<'v>,
#[starlark(require = pos)] argv: UnpackList<Value<'v>>,
#[starlark(require = named)] env: Option<SmallMap<String, Value<'v>>>,
#[starlark(require = named)] cwd: Option<String>,
) -> anyhow::Result<NoneType> {
let argv: Vec<String> = argv
.items
.iter()
.map(|v| coerce_path_string(*v))
.collect::<anyhow::Result<Vec<_>>>()?;
let mut env_strings: SmallMap<String, String> = SmallMap::new();
if let Some(env) = env {
for (k, v) in env {
env_strings.insert(k, coerce_path_string(v)?);
}
}
run_in_container(&argv, env_strings, cwd.as_deref().unwrap_or("/build"))?;
Ok(NoneType)
}
}
fn run_in_container(
argv: &[String],
env_overrides: SmallMap<String, String>,
cwd: &str,
) -> anyhow::Result<()> {
let (container, env) = with_current(|runtime| {
let mut env: Vec<(String, String)> = runtime
.base_env
.iter()
.cloned()
.map(|(k, v)| {
if k == "PATH" {
(k, runtime.base_path.clone())
} else {
(k, v)
}
})
.collect();
for (k, v) in env_overrides {
if let Some(slot) = env.iter_mut().find(|(existing, _)| existing == &k) {
slot.1 = v;
} else {
env.push((k, v));
}
}
(runtime.container.clone(), env)
})?;
container.borrow().exec(argv, &env, cwd)
}
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub struct PackageContext {
dest_dir: String,
}
impl PackageContext {
pub fn new(dest_dir: String) -> Self {
Self { dest_dir }
}
}
impl std::fmt::Display for PackageContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "pkg")
}
}
starlark::starlark_simple_value!(PackageContext);
#[starlark_value(type = "package_context")]
impl<'v> StarlarkValue<'v> for PackageContext {
fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option<Value<'v>> {
match attr {
"dest_dir" => Some(heap.alloc(Path::new(self.dest_dir.clone()))),
_ => None,
}
}
fn has_attr(&self, attr: &str, _heap: &'v Heap) -> bool {
attr == "dest_dir"
}
fn dir_attr(&self) -> Vec<String> {
vec!["dest_dir".to_owned()]
}
}
pub fn invoke_phase(func: &OwnedFrozenValue, args: &[PhaseArg]) -> anyhow::Result<()> {
let module = Module::new();
let mut eval = Evaluator::new(&module);
let allocated: Vec<Value<'_>> = args
.iter()
.map(|arg| match arg {
PhaseArg::Ctx(ctx) => module.heap().alloc(ctx.clone()),
PhaseArg::Pkg(pkg) => module.heap().alloc(pkg.clone()),
})
.collect();
eval.eval_function(func.value(), &allocated, &[])
.map_err(|err| anyhow!("{err}"))?;
Ok(())
}
pub enum PhaseArg {
Ctx(PhaseContext),
Pkg(PackageContext),
}
-222
View File
@@ -1,222 +0,0 @@
use crate::recipe::{PackageRecipe, RecipeSet, SourceRecipe, ToolRecipe};
use petgraph::{
Direction,
graph::{DiGraph, NodeIndex},
};
use smallvec::{SmallVec, smallvec};
use std::{
cmp::Reverse,
collections::{BinaryHeap, HashMap, HashSet},
};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum PlanError {
#[error("missing source recipe '{0}'")]
MissingSource(String),
#[error("missing tool recipe '{0}'")]
MissingTool(String),
#[error("missing package recipe '{0}'")]
MissingPackage(String),
#[error("plan cycle detected")]
CycleDetected,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum PlanKey {
SourceFetch(String),
SourcePatch(String),
SourcePrepare(String),
ToolConfigure(String),
ToolBuild(String),
ToolInstall(String),
PkgConfigure(String),
PkgBuild(String),
PkgInstall(String),
PkgPackage(String),
}
impl PlanKey {
fn weight(&self) -> i8 {
match self {
PlanKey::SourceFetch(_) => 0,
PlanKey::SourcePatch(_) => 1,
PlanKey::SourcePrepare(_) => 2,
PlanKey::ToolConfigure(_) => 3,
PlanKey::ToolBuild(_) => 4,
PlanKey::ToolInstall(_) => 5,
PlanKey::PkgConfigure(_) => 6,
PlanKey::PkgBuild(_) => 7,
PlanKey::PkgInstall(_) => 8,
PlanKey::PkgPackage(_) => 9,
}
}
fn dependencies(&self, recipes: &RecipeSet) -> Result<SmallVec<[PlanKey; 8]>, PlanError> {
match self {
PlanKey::SourceFetch(_) => Ok(smallvec![]),
PlanKey::SourcePatch(recipe) => Ok(smallvec![PlanKey::SourceFetch(recipe.clone())]),
PlanKey::SourcePrepare(recipe) => Ok(smallvec![PlanKey::SourcePatch(recipe.clone())]),
PlanKey::ToolConfigure(recipe) => {
let recipe = recipes
.tool(recipe)
.ok_or(PlanError::MissingTool(recipe.clone()))?;
let source_deps = recipe.sources.iter().map(|name| {
recipes
.source(name)
.map(|_| PlanKey::SourcePrepare(name.to_string()))
.ok_or(PlanError::MissingSource(name.to_string()))
});
let tool_deps = recipe.tools_wanted.iter().map(|name| {
recipes
.tool(name)
.map(|_| PlanKey::ToolInstall(name.to_string()))
.ok_or(PlanError::MissingTool(name.to_string()))
});
let pkg_deps = recipe.pkgs_wanted.iter().map(|name| {
recipes
.package(name)
.map(|_| PlanKey::PkgPackage(name.to_string()))
.ok_or(PlanError::MissingPackage(name.to_string()))
});
source_deps.chain(tool_deps).chain(pkg_deps).collect()
}
PlanKey::ToolBuild(recipe) => Ok(smallvec![PlanKey::ToolConfigure(recipe.clone())]),
PlanKey::ToolInstall(recipe) => Ok(smallvec![PlanKey::ToolBuild(recipe.clone())]),
PlanKey::PkgConfigure(recipe) => {
let recipe = recipes
.package(recipe)
.ok_or(PlanError::MissingPackage(recipe.clone()))?;
let source_deps = recipe.sources.iter().map(|name| {
recipes
.source(name)
.map(|_| PlanKey::SourcePrepare(name.to_string()))
.ok_or(PlanError::MissingSource(name.to_string()))
});
let tool_deps = recipe.tools_wanted.iter().map(|name| {
recipes
.tool(name)
.map(|_| PlanKey::ToolInstall(name.to_string()))
.ok_or(PlanError::MissingTool(name.to_string()))
});
let pkg_deps = recipe.pkgs_wanted.iter().map(|name| {
recipes
.package(name)
.map(|_| PlanKey::PkgPackage(name.to_string()))
.ok_or(PlanError::MissingPackage(name.to_string()))
});
source_deps.chain(tool_deps).chain(pkg_deps).collect()
}
PlanKey::PkgBuild(recipe) => Ok(smallvec![PlanKey::PkgConfigure(recipe.clone())]),
PlanKey::PkgInstall(recipe) => Ok(smallvec![PlanKey::PkgBuild(recipe.clone())]),
PlanKey::PkgPackage(recipe) => Ok(smallvec![PlanKey::PkgInstall(recipe.clone())]),
}
}
fn is_active(&self) -> bool {
true
}
}
pub struct Plan<'a> {
recipes: &'a RecipeSet<'a>,
wanted: HashSet<PlanKey>,
}
impl<'a> Plan<'a> {
pub fn new(recipes: &'a RecipeSet) -> Self {
Self {
recipes,
wanted: HashSet::new(),
}
}
pub fn add_wanted(&mut self, key: PlanKey) {
self.wanted.insert(key);
}
pub fn steps(&self) -> Result<Vec<PlanKey>, PlanError> {
let mut stack: Vec<_> = self.wanted.iter().cloned().collect();
let mut graph: DiGraph<_, ()> = DiGraph::new();
let mut nodes = HashMap::new();
while let Some(node) = stack.pop() {
let node_idx = match nodes.get(&node) {
Some(&idx) => idx,
None => {
let idx = graph.add_node(node.clone());
nodes.insert(node.clone(), idx);
idx
}
};
for dep in node.dependencies(self.recipes)? {
let dep_idx = match nodes.get(&dep) {
Some(&idx) => idx,
None => {
let idx = graph.add_node(dep.clone());
nodes.insert(dep.clone(), idx);
stack.push(dep);
idx
}
};
graph.update_edge(dep_idx, node_idx, ());
}
}
// petgraph::algo::toposort(&graph, None)
// .and_then(|nodes| {
// Ok(nodes
// .iter()
// .map(|&k| graph[k])
// .filter(|node| node.is_active())
// .collect())
// })
// .map_err(|_| PlanError::CycleDetected)
let mut in_degree: HashMap<NodeIndex, usize> = graph
.node_indices()
.map(|i| (i, graph.neighbors_directed(i, Direction::Incoming).count()))
.collect();
let mut heap: BinaryHeap<(Reverse<i8>, NodeIndex)> = in_degree
.iter()
.filter(|&(_, d)| *d == 0)
.map(|(&i, _)| (Reverse(graph[i].weight()), i))
.collect();
let mut result = Vec::with_capacity(graph.node_count());
while let Some((_, idx)) = heap.pop() {
result.push(graph[idx].clone());
for neighbor in graph.neighbors_directed(idx, Direction::Outgoing) {
let d = in_degree.get_mut(&neighbor).unwrap();
*d -= 1;
if *d == 0 {
heap.push((Reverse(graph[neighbor].weight()), neighbor));
}
}
}
if result.len() != graph.node_count() {
Err(PlanError::CycleDetected)
} else {
result.retain(|node| node.is_active());
Ok(result)
}
}
}
-256
View File
@@ -1,256 +0,0 @@
use anyhow::Context;
use starlark::{
environment::{FrozenModule, GlobalsBuilder, Module},
eval,
values::{
UnpackValue,
typing::{FrozenStarlarkCallable, StarlarkCallable, StarlarkCallableParamSpec},
},
};
use std::{
collections::HashMap,
path::{Path, PathBuf},
};
use crate::eval::{
Config, Metadata, TarballSource, UnpackCloned, eval_files, recipe_globals, types_globals,
};
pub struct SourceRecipe {
pub name: String,
pub source: Box<dyn Source>,
}
pub trait Source {}
pub struct ToolRecipe {
pub name: String,
pub sources: Vec<String>,
pub tools_wanted: Vec<String>,
pub pkgs_wanted: Vec<String>,
}
#[derive(Debug)]
pub struct PackageRecipe {
pub name: String,
pub meta: Option<Metadata>,
pub version: String,
pub revision: u32,
pub sources: Vec<String>,
pub tools_wanted: Vec<String>,
pub pkgs_wanted: Vec<String>,
pub module: FrozenModule,
pub configure: Option<FrozenStarlarkCallable>,
pub build: Option<FrozenStarlarkCallable>,
pub install: Option<FrozenStarlarkCallable>,
}
pub struct RecipeSet<'a> {
sources: HashMap<String, SourceRecipe>,
tools: HashMap<String, ToolRecipe>,
pub packages: HashMap<String, PackageRecipe>,
config: &'a Config,
}
impl<'a> RecipeSet<'a> {
fn add_source(&mut self, name: &str, recipe: SourceRecipe) -> anyhow::Result<()> {
if self.sources.insert(name.to_string(), recipe).is_some() {
anyhow::bail!("source '{name}' already exists");
}
Ok(())
}
fn add_tool(&mut self, name: &str, recipe: ToolRecipe) -> anyhow::Result<()> {
if self.tools.insert(name.to_string(), recipe).is_some() {
anyhow::bail!("tool '{name}' already exists");
}
Ok(())
}
fn add_package(&mut self, name: &str, recipe: PackageRecipe) -> anyhow::Result<()> {
if self.packages.insert(name.to_string(), recipe).is_some() {
anyhow::bail!("package '{name}' already exists");
}
Ok(())
}
fn load_tool_recipe(&mut self, name: &str, path: &Path) -> anyhow::Result<()> {
Ok(())
}
fn load_package_recipe(&mut self, name: &str, path: &Path) -> anyhow::Result<()> {
let module = eval_files(
&[path],
&GlobalsBuilder::standard()
.with(types_globals)
.with(recipe_globals)
.build(),
None,
Some(self.config),
None,
)?;
let module = module.freeze().map_err(|err| anyhow::anyhow!("{err:?}"))?;
let version: String = get_value(&module, "version")?;
let revision: u32 = get_value_option(&module, "revision")?.unwrap_or(1);
let metadata: Option<Metadata> = get_value_option(&module, "metadata")?;
let source: TarballSource = get_value(&module, "source")?;
let configure = get_frozen_callable(&module, "configure")?;
let build = get_frozen_callable(&module, "build")?;
let install = get_frozen_callable(&module, "install")?;
self.add_source(
name,
SourceRecipe {
name: name.to_string(),
source: Box::new(source),
},
)?;
self.add_package(
name,
PackageRecipe {
name: name.to_string(),
meta: metadata,
version,
revision,
sources: vec![name.to_string()],
tools_wanted: vec![],
pkgs_wanted: vec![],
module,
configure,
build,
install,
},
)
}
pub fn new(config: &'a Config) -> Self {
Self {
sources: HashMap::new(),
tools: HashMap::new(),
packages: HashMap::new(),
config,
}
}
pub fn load_recipes(
&mut self,
recipes_dir: &Path,
host_recipes_dir: &Path,
) -> anyhow::Result<()> {
for (dir, tool_recipe) in [(recipes_dir, false), (host_recipes_dir, true)] {
for entry in std::fs::read_dir(dir)? {
let entry = entry.context("reading directory entry")?;
if let Some((name, path)) = get_recipe_name_and_patch(&entry)? {
if tool_recipe {
self.load_tool_recipe(&name, &path)?;
} else {
self.load_package_recipe(&name, &path)?;
}
}
}
}
Ok(())
}
pub fn source(&self, name: &str) -> Option<&SourceRecipe> {
self.sources.get(name)
}
pub fn tool(&self, name: &str) -> Option<&ToolRecipe> {
self.tools.get(name)
}
pub fn package(&self, name: &str) -> Option<&PackageRecipe> {
self.packages.get(name)
}
}
fn get_recipe_name_and_patch(
entry: &std::fs::DirEntry,
) -> anyhow::Result<Option<(String, PathBuf)>> {
let file_type = entry.file_type()?;
let path = entry.path();
if file_type.is_dir() {
let recipe_path = path.join("recipe.star");
if recipe_path.exists() {
return Ok(Some((
entry.file_name().to_str().unwrap_or("").to_string(),
recipe_path,
)));
}
} else {
let name = path.file_stem().unwrap().to_str().unwrap_or("").to_string();
let extension = path
.extension()
.ok_or(anyhow::anyhow!("File did not have an extension"))?;
if extension == "star" {
return Ok(Some((name, path)));
}
}
Ok(None)
}
fn get_value_option<T: UnpackCloned>(
module: &FrozenModule,
name: &str,
) -> anyhow::Result<Option<T>> {
module
.get_option(name)?
.map(|value| {
T::unpack_cloned(value.value()).ok_or_else(|| {
anyhow::anyhow!(
"`{name}` should be of type `{}` but got `{}`",
T::starlark_type_repr(),
value.value().get_type()
)
})
})
.transpose()
}
fn get_value<T: UnpackCloned>(module: &FrozenModule, name: &str) -> anyhow::Result<T> {
let value = module
.get_option(name)?
.ok_or_else(|| anyhow::anyhow!("`{name}` is required"))?;
T::unpack_cloned(value.value()).ok_or_else(|| {
anyhow::anyhow!(
"`{name}` should be of type `{}` but got `{}`",
T::starlark_type_repr(),
value.value().get_type()
)
})
}
fn get_frozen_callable<P: StarlarkCallableParamSpec>(
module: &FrozenModule,
name: &str,
) -> anyhow::Result<Option<FrozenStarlarkCallable<P>>> {
let Some(value) = module.get_option(name)? else {
return Ok(None);
};
let callable = StarlarkCallable::unpack_value(value.value())
.map_err(|err| anyhow::anyhow!("{err}"))?
.ok_or_else(|| {
anyhow::anyhow!(
"`{name}` should be callable but got `{}`",
value.value().get_type()
)
})?;
callable
.unpack_frozen()
.ok_or_else(|| anyhow::anyhow!("`{name}` was callable but not frozen"))
.map(Some)
}
+71
View File
@@ -0,0 +1,71 @@
use allocative::Allocative;
use starlark::values::StarlarkValue;
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub struct Metadata {
maintainer: Option<String>,
description: Option<String>,
license: Option<String>,
website: Option<String>,
}
impl std::fmt::Display for Metadata {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "metadata")
}
}
starlark::starlark_simple_value!(Metadata);
#[starlark_value(type = "metadata")]
impl<'v> StarlarkValue<'v> for Metadata {}
impl Metadata {
pub fn new(
maintainer: Option<String>,
description: Option<String>,
license: Option<String>,
website: Option<String>,
) -> Self {
Self {
maintainer,
description,
license,
website,
}
}
pub fn maintainer(&self) -> Option<&str> {
self.maintainer.as_deref()
}
pub fn description(&self) -> Option<&str> {
self.description.as_deref()
}
pub fn license(&self) -> Option<&str> {
self.license.as_deref()
}
pub fn website(&self) -> Option<&str> {
self.website.as_deref()
}
pub fn inherit(&self, from: &Self) -> Self {
Self {
maintainer: self
.maintainer
.as_ref()
.or(from.maintainer.as_ref())
.cloned(),
description: self
.description
.as_ref()
.or(from.description.as_ref())
.cloned(),
license: self.license.as_ref().or(from.license.as_ref()).cloned(),
website: self.website.as_ref().or(from.website.as_ref()).cloned(),
}
}
}
+603
View File
@@ -0,0 +1,603 @@
mod metadata;
mod source;
mod subpackage;
use anyhow::{Context, bail};
use starlark::{
environment::{FrozenModule, Module},
eval::Evaluator,
values::{
OwnedFrozenValue, UnpackValue, ValueLike, dict::DictRef, list::ListRef,
typing::StarlarkCallable,
},
};
use std::{
collections::{HashMap, HashSet},
path::{Path, PathBuf},
};
use walkdir::WalkDir;
use crate::{
eval::{self, ExtractError},
options::Options,
};
pub use metadata::Metadata;
pub use source::{GitSource, Source, TarballSource};
pub use subpackage::Subpackage;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RecipeKind {
Package,
HostPackage,
}
impl RecipeKind {
pub fn key(self, name: &str) -> String {
match self {
Self::Package => name.to_string(),
Self::HostPackage => format!("host:{name}"),
}
}
pub fn slug(self, name: &str) -> String {
self.key(name).replace(':', "-")
}
}
#[derive(Debug)]
pub enum Sources {
Single(source::Source),
Multiple(HashMap<String, source::Source>),
}
impl Sources {
pub fn entries(&self) -> Vec<(Option<&str>, &source::Source)> {
match self {
Self::Single(source) => vec![(None, source)],
Self::Multiple(sources) => {
let mut entries = sources
.iter()
.map(|(name, source)| (Some(name.as_str()), source))
.collect::<Vec<_>>();
entries.sort_by_key(|(name, _)| *name);
entries
}
}
}
}
pub struct Recipe {
/// Recipe name without namespace prefix.
name: String,
/// Path to the recipe's .star file.
path: PathBuf,
/// What kind of a recipe is that?
kind: RecipeKind,
/// Version shared by every package output of this recipe.
version: String,
/// Revision shared by every package output of this recipe.
revision: i32,
/// List of sources required to build this recipe.
sources: Sources,
/// All packages produced by this recipe.
/// This is empty for host recipes.
outputs: Vec<OutputPackage>,
/// Host packages requires for this recipe.
host_deps: Vec<String>,
/// Packages installed to the system root during build of the recipe, but
/// not listed as part of the `apk` dependencies.
build_deps: Vec<String>,
/// Packages installed to the system root during build of the recipe AND
/// listed as part of the `apk` dependencies.
deps: Vec<String>,
/// Packages NOT installed to the system root during build, only listed
/// as part of the `apk` dependencies.
run_deps: Vec<String>,
/// Starlark phase functions defined by the recipe.
phases: RecipePhases,
}
impl Recipe {
pub fn load(
path: &Path,
name: &str,
kind: RecipeKind,
options: &Options,
lib: Option<&FrozenModule>,
) -> anyhow::Result<Option<Self>> {
let module = eval::eval_file(path, Some(options), lib)
.with_context(|| format!("evaluating recipe {}", path.display()))?;
if !Self::eval_build_if(&module)? {
return Ok(None);
}
let version = eval::extract_string(&module, "version")
.map_err(|e| anyhow::anyhow!("field `version`: {e}"))?;
let revision = match eval::extract_i32(&module, "revision") {
Ok(v) => v,
Err(ExtractError::NotFound) => 1,
Err(e) => bail!("field `revision`: {e}"),
};
let metadata = match module.get("metadata") {
None => Metadata::new(None, None, None, None),
Some(value) => value
.downcast_ref::<Metadata>()
.ok_or_else(|| anyhow::anyhow!("field `metadata`: expected a metadata value"))?
.clone(),
};
let source_value = module.get("source");
let sources_value = module.get("sources");
let sources = match (source_value, sources_value) {
(None, None) => bail!("recipe must define either `source` or `sources`"),
(Some(_), Some(_)) => {
bail!("recipe must define exactly one of `source` or `sources`, not both")
}
(Some(value), None) => {
let source = value
.downcast_ref::<Source>()
.ok_or_else(|| anyhow::anyhow!("field `source`: expected a source value"))?
.clone();
Sources::Single(source)
}
(None, Some(value)) => {
let dict = DictRef::from_value(value).ok_or_else(|| {
anyhow::anyhow!("field `sources`: expected a dict of name -> source")
})?;
if dict.iter().len() == 0 {
bail!("field `sources`: must contain at least one entry");
}
let mut map: HashMap<String, source::Source> = HashMap::new();
for (key, value) in dict.iter() {
let key_str = key
.unpack_str()
.ok_or_else(|| anyhow::anyhow!("field `sources`: keys must be strings"))?;
if !Self::is_valid_source_name(key_str) {
bail!(
"field `sources`: invalid source name `{key_str}` (allowed: letters, digits, `-`, `_`; must start with a letter or digit)"
);
}
let source = value.downcast_ref::<Source>().ok_or_else(|| {
anyhow::anyhow!("field `sources`: entry `{key_str}` is not a source value")
})?;
if map.insert(key_str.to_owned(), source.clone()).is_some() {
bail!("field `sources`: duplicate key `{key_str}`");
}
}
Sources::Multiple(map)
}
};
let host_deps = Self::optional_string_list(&module, "host_deps")?;
let build_deps = Self::optional_string_list(&module, "build_deps")?;
let deps = Self::optional_string_list(&module, "deps")?;
let run_deps = Self::optional_string_list(&module, "run_deps")?;
let recipe_key = kind.key(name);
let outputs = match kind {
RecipeKind::Package => {
let mut outputs = vec![OutputPackage {
recipe: recipe_key.clone(),
name: name.to_owned(),
metadata: metadata.clone(),
file_globs: Vec::new(),
base: true,
}];
if let Some(value) = module.get("subpackages") {
let list = ListRef::from_value(value)
.ok_or_else(|| anyhow::anyhow!("field `subpackages`: expected a list"))?;
for item in list.iter() {
let sub = item.downcast_ref::<Subpackage>().ok_or_else(|| {
anyhow::anyhow!(
"field `subpackages`: each entry must be a subpackage value"
)
})?;
Self::check_subpackage_file_globs(sub.name(), sub.file_globs())?;
outputs.push(OutputPackage {
recipe: recipe_key.clone(),
name: sub.name().to_owned(),
metadata: sub.metadata().inherit(&metadata),
file_globs: sub.file_globs().to_vec(),
base: false,
});
}
}
outputs
}
RecipeKind::HostPackage => {
if module.get("subpackages").is_some() {
bail!("host recipes cannot declare `subpackages`");
}
Vec::new()
}
};
let module = module
.freeze()
.map_err(|err| anyhow::anyhow!("freezing recipe module {}: {err:?}", path.display()))?;
let phases = RecipePhases::load(&module)?;
let recipe = Recipe {
name: name.to_owned(),
path: path.to_path_buf(),
kind,
version,
revision,
sources,
outputs,
host_deps,
build_deps,
deps,
run_deps,
phases,
};
Ok(Some(recipe))
}
pub fn phases(&self) -> &RecipePhases {
&self.phases
}
pub fn key(&self) -> String {
self.kind.key(&self.name)
}
pub fn slug(&self) -> String {
self.kind.slug(&self.name)
}
pub fn data_dir(&self) -> Option<&Path> {
if self.path.file_name().is_some_and(|n| n == "recipe.star") {
self.path.parent()
} else {
None
}
}
pub fn files_dir(&self) -> Option<PathBuf> {
let candidate = self.data_dir()?.join("files");
if candidate.is_dir() {
Some(candidate)
} else {
None
}
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn kind(&self) -> RecipeKind {
self.kind
}
pub fn version(&self) -> &str {
&self.version
}
pub fn revision(&self) -> i32 {
self.revision
}
pub fn sources(&self) -> &Sources {
&self.sources
}
pub fn outputs(&self) -> &[OutputPackage] {
&self.outputs
}
pub fn base_output(&self) -> Option<&OutputPackage> {
self.outputs.iter().find(|output| output.is_base())
}
pub fn host_deps(&self) -> &[String] {
&self.host_deps
}
pub fn build_deps(&self) -> &[String] {
&self.build_deps
}
pub fn deps(&self) -> &[String] {
&self.deps
}
pub fn run_deps(&self) -> &[String] {
&self.run_deps
}
fn optional_string_list(module: &Module, key: &str) -> anyhow::Result<Vec<String>> {
match eval::extract_string_list(module, key) {
Ok(v) => Ok(v),
Err(ExtractError::NotFound) => Ok(Vec::new()),
Err(e) => Err(anyhow::anyhow!("field `{key}`: {e}")),
}
}
/// Evaluates "build_if" functions.
fn eval_build_if(module: &Module) -> anyhow::Result<bool> {
let Some(value) = module.get("build_if") else {
return Ok(true);
};
if let Some(b) = value.unpack_bool() {
return Ok(b);
}
let callable: Option<StarlarkCallable<'_>> = StarlarkCallable::unpack_value_opt(value);
if callable.is_none() {
bail!("field `build_if`: expected a bool or a callable returning a bool");
}
let mut eval = Evaluator::new(module);
let result = eval
.eval_function(value, &[], &[])
.map_err(|err| anyhow::anyhow!("calling `build_if`: {err}"))?;
result
.unpack_bool()
.ok_or_else(|| anyhow::anyhow!("field `build_if`: must return a bool"))
}
fn is_valid_source_name(name: &str) -> bool {
let mut chars = name.chars();
let Some(first) = chars.next() else {
return false;
};
if !first.is_ascii_alphanumeric() {
return false;
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}
fn check_subpackage_file_globs(name: &str, file_globs: &[String]) -> anyhow::Result<()> {
if file_globs.is_empty() {
bail!("subpackage '{name}' must declare at least one file glob");
}
for glob in file_globs {
if glob.is_empty() {
bail!("subpackage '{name}' has an empty file glob");
}
if glob.starts_with('/') {
bail!("subpackage '{name}' glob '{glob}' must be relative to the package dest dir");
}
if Path::new(glob)
.components()
.any(|component| matches!(component, std::path::Component::ParentDir))
{
bail!("subpackage '{name}' glob '{glob}' must not contain '..'");
}
}
Ok(())
}
}
pub struct RecipePhases {
prepare: Option<OwnedFrozenValue>,
configure: Option<OwnedFrozenValue>,
build: OwnedFrozenValue,
install: OwnedFrozenValue,
}
impl RecipePhases {
fn load(module: &FrozenModule) -> anyhow::Result<Self> {
Ok(Self {
prepare: optional_phase_function(module, "prepare")?,
configure: optional_phase_function(module, "configure")?,
build: required_phase_function(module, "build")?,
install: required_phase_function(module, "install")?,
})
}
pub fn prepare(&self) -> Option<&OwnedFrozenValue> {
self.prepare.as_ref()
}
pub fn configure(&self) -> Option<&OwnedFrozenValue> {
self.configure.as_ref()
}
pub fn build(&self) -> &OwnedFrozenValue {
&self.build
}
pub fn install(&self) -> &OwnedFrozenValue {
&self.install
}
}
fn optional_phase_function(
module: &FrozenModule,
name: &str,
) -> anyhow::Result<Option<OwnedFrozenValue>> {
let Some(value) = module
.get_option(name)
.with_context(|| format!("field `{name}`"))?
else {
return Ok(None);
};
validate_phase_function(name, &value)?;
Ok(Some(value))
}
fn required_phase_function(module: &FrozenModule, name: &str) -> anyhow::Result<OwnedFrozenValue> {
let value = module
.get_option(name)
.with_context(|| format!("field `{name}`"))?
.ok_or_else(|| anyhow::anyhow!("field `{name}`: missing"))?;
validate_phase_function(name, &value)?;
Ok(value)
}
fn validate_phase_function(name: &str, value: &OwnedFrozenValue) -> anyhow::Result<()> {
let callable: Option<StarlarkCallable<'_>> = StarlarkCallable::unpack_value_opt(value.value());
if callable.is_none() {
bail!("field `{name}`: expected a callable value");
}
Ok(())
}
#[derive(Clone)]
pub struct OutputPackage {
/// Canonical key of the owning recipe.
recipe: String,
/// Name of the output package.
name: String,
/// Metadata attached to the output package.
metadata: Metadata,
/// File globs claimed from the base package dest dir.
file_globs: Vec<String>,
/// Whether this is the recipe's base output.
base: bool,
}
impl OutputPackage {
pub fn key(&self) -> String {
self.name.clone()
}
pub fn recipe(&self) -> &str {
&self.recipe
}
pub fn name(&self) -> &str {
&self.name
}
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
pub fn file_globs(&self) -> &[String] {
&self.file_globs
}
pub fn is_base(&self) -> bool {
self.base
}
}
pub struct RecipeSet {
recipes: HashMap<String, Recipe>,
outputs: HashMap<String, OutputPackage>,
skipped: HashSet<String>,
}
impl RecipeSet {
pub fn load(
root_path: &Path,
options: &Options,
lib: Option<&FrozenModule>,
) -> anyhow::Result<Self> {
let mut recipes = HashMap::new();
let mut outputs = HashMap::new();
let mut skipped: HashSet<String> = HashSet::new();
for (path, kind) in [
("recipes", RecipeKind::Package),
("host-recipes", RecipeKind::HostPackage),
] {
let recipes_dir = root_path.join(path);
if !recipes_dir.exists() {
continue;
}
for (name, path) in discover_recipes(&recipes_dir)? {
let key = kind.key(&name);
let loaded = Recipe::load(&path, &name, kind, options, lib)
.with_context(|| format!("loading recipe `{name}`"))?;
let Some(recipe) = loaded else {
skipped.insert(key);
continue;
};
if recipes.insert(key.clone(), recipe).is_some() {
bail!("duplicate recipe `{key}`");
}
}
}
for recipe in recipes.values() {
for output in &recipe.outputs {
let key = recipe.kind.key(&output.name);
if outputs.insert(key.clone(), output.clone()).is_some() {
bail!("duplicate package output `{key}`");
}
}
}
Ok(Self {
recipes,
outputs,
skipped,
})
}
pub fn recipe(&self, key: &str) -> anyhow::Result<&Recipe> {
self.recipes
.get(key)
.ok_or_else(|| anyhow::anyhow!("unknown recipe `{key}`"))
}
pub fn output(&self, key: &str) -> anyhow::Result<&OutputPackage> {
self.outputs
.get(key)
.ok_or_else(|| anyhow::anyhow!("unknown output package `{key}`"))
}
pub fn is_skipped(&self, key: &str) -> bool {
self.skipped.contains(key)
}
}
fn discover_recipes(dir: &Path) -> anyhow::Result<HashMap<String, PathBuf>> {
let mut recipes: HashMap<String, PathBuf> = HashMap::new();
let walker = WalkDir::new(dir).follow_links(false);
for entry in walker {
let entry =
entry.with_context(|| format!("walking recipes directory {}", dir.display()))?;
if !entry.file_type().is_file() {
continue;
}
let path = entry.path();
let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
continue;
};
let name = if file_name == "recipe.star" {
let Some(parent_name) = path
.parent()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
else {
continue;
};
parent_name.to_owned()
} else if let Some(stem) = file_name.strip_suffix(".star") {
stem.to_owned()
} else {
continue;
};
if let Some(existing) = recipes.insert(name.clone(), path.to_path_buf()) {
bail!(
"recipe `{name}` is defined twice: {} and {}",
existing.display(),
recipes[&name].display(),
);
}
}
Ok(recipes)
}
+133
View File
@@ -0,0 +1,133 @@
use allocative::Allocative;
use starlark::values::StarlarkValue;
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub struct TarballSource {
url: String,
sha256: String,
strip_components: u32,
patches: Vec<String>,
}
impl std::fmt::Display for TarballSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "tarball_source")
}
}
starlark::starlark_simple_value!(TarballSource);
#[starlark_value(type = "tarball_source")]
impl<'v> StarlarkValue<'v> for TarballSource {}
impl TarballSource {
pub fn new(url: String, sha256: String, strip_components: u32, patches: Vec<String>) -> Self {
Self {
url,
sha256,
strip_components,
patches,
}
}
pub fn url(&self) -> &str {
&self.url
}
pub fn sha256(&self) -> &str {
&self.sha256
}
pub fn strip_components(&self) -> u32 {
self.strip_components
}
pub fn patches(&self) -> &[String] {
&self.patches
}
}
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub struct GitSource {
url: String,
commit: String,
patches: Vec<String>,
}
impl std::fmt::Display for GitSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "git_source")
}
}
starlark::starlark_simple_value!(GitSource);
#[starlark_value(type = "git_source")]
impl<'v> StarlarkValue<'v> for GitSource {}
impl GitSource {
pub fn new(url: String, commit: String, patches: Vec<String>) -> Self {
Self {
url,
commit,
patches,
}
}
pub fn url(&self) -> &str {
&self.url
}
pub fn commit(&self) -> &str {
&self.commit
}
pub fn patches(&self) -> &[String] {
&self.patches
}
}
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub enum Source {
Tarball(TarballSource),
Git(GitSource),
}
impl std::fmt::Display for Source {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "source")
}
}
starlark::starlark_simple_value!(Source);
#[starlark_value(type = "source")]
impl<'v> StarlarkValue<'v> for Source {}
impl Source {
pub fn url(&self) -> &str {
match self {
Self::Tarball(source) => source.url(),
Self::Git(source) => source.url(),
}
}
pub fn cache_key(&self) -> &str {
match self {
Self::Tarball(source) => source.sha256(),
Self::Git(source) => source.commit(),
}
}
pub fn is_unknown_cache_key(&self) -> bool {
matches!(self.cache_key(), "?" | "???")
}
pub fn patches(&self) -> &[String] {
match self {
Self::Tarball(source) => source.patches(),
Self::Git(source) => source.patches(),
}
}
}
+45
View File
@@ -0,0 +1,45 @@
use allocative::Allocative;
use starlark::values::StarlarkValue;
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
use crate::recipe::Metadata;
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
pub struct Subpackage {
name: String,
file_globs: Vec<String>,
metadata: Metadata,
}
impl Subpackage {
pub fn new(name: String, file_globs: Vec<String>, metadata: Metadata) -> Self {
Self {
name,
file_globs,
metadata,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn file_globs(&self) -> &[String] {
&self.file_globs
}
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
}
impl std::fmt::Display for Subpackage {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "subpackage")
}
}
starlark::starlark_simple_value!(Subpackage);
#[starlark_value(type = "subpackage")]
impl<'v> StarlarkValue<'v> for Subpackage {}