3 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
36 changed files with 14098 additions and 283 deletions
Generated
+1
View File
@@ -458,6 +458,7 @@ dependencies = [
"anyhow",
"clap",
"hex",
"libc",
"reqwest",
"serde",
"serde_json",
+1
View File
@@ -8,6 +8,7 @@ license = "MIT"
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",
+6
View File
@@ -16,8 +16,10 @@ RUN apk upgrade --no-cache && apk add --no-cache \
file \
findutils \
flex \
gawk \
gettext-dev \
git \
grep \
gzip \
elfutils-dev \
gmp-dev \
@@ -26,12 +28,16 @@ RUN apk upgrade --no-cache && apk add --no-cache \
libtool \
linux-headers \
meson \
mtools \
nasm \
ncurses \
ninja \
openssl \
openssl-dev \
patch \
pkgconf \
python3 \
rsync \
tar \
texinfo \
xz \
+17 -3
View File
@@ -3,10 +3,15 @@ container_image = "local/distro-builder:latest"
container_dockerfile = "Dockerfile"
arch = "x86_64"
libc = "musl"
libc = "glibc"
if libc == "glibc":
env = "gnu"
else:
env = libc
host_cflags = "-O2 -pipe"
host_cxxflags = ""
host_cxxflags = host_cflags
host_ldflags = "-Wl,-O1 -Wl,--sort-common -Wl,--as-needed"
target_cflags = host_cflags
@@ -22,7 +27,7 @@ if arch == "x86_64":
options = dict(
target_arch = arch,
target_triple = f"{arch}-linux-{libc}",
target_triple = f"{arch}-linux-{env}",
host_cflags = host_cflags,
host_cxxflags = host_cxxflags,
@@ -33,4 +38,13 @@ options = dict(
ldflags = target_ldflags,
libc = libc,
prefix = "/usr",
bindir = "/usr/bin",
sbindir = "/usr/bin",
libdir = "/usr/lib",
libexecdir = "/usr/libexec",
includedir = "/usr/include",
sysconfdir = "/etc",
localstatedir = "/var",
)
+9 -7
View File
@@ -5,15 +5,15 @@ metadata = meta(
license = "GPL-3.0-or-later",
)
source = tarball_source(
url = "https://ftp.gnu.org/gnu/binutils/binutils-" + version + ".tar.xz",
url = f"https://ftp.gnu.org/gnu/binutils/binutils-{version}.tar.xz",
sha256 = "d75a94f4d73e7a4086f7513e67e439e8fcdcbb726ffe63f4661744e6256b2cf2",
strip_components = 1,
)
def configure(ctx):
ctx.run([
ctx.source_dir + "/configure",
"--prefix=" + ctx.prefix,
configure_args = [
ctx.source_dir / "configure",
"--prefix=/",
"--target=" + options.target_triple,
"--with-sysroot=" + ctx.sysroot,
"--with-pic",
@@ -26,11 +26,13 @@ def configure(ctx):
"--enable-relro",
"--enable-separate-code",
"--enable-threads",
# gprofng's libcollector does not build against musl.
"--disable-gprofng",
"--disable-nls",
"--disable-werror",
], env = {
# gprofng's libcollector relies on glibc-specific internals.
"--disable-gprofng",
]
ctx.run(configure_args, env = {
"CFLAGS": options.host_cflags,
"CXXFLAGS": options.host_cxxflags,
"LDFLAGS": options.host_ldflags,
+2 -2
View File
@@ -13,9 +13,9 @@ host_deps = ["binutils"]
def configure(ctx):
ctx.run([
ctx.source_dir + "/configure",
ctx.source_dir / "configure",
"--target=" + options.target_triple,
"--prefix=" + ctx.prefix,
"--prefix=/",
"--with-sysroot=" + ctx.sysroot,
"--without-headers",
"--with-newlib",
+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})
+67 -9
View File
@@ -1,4 +1,4 @@
# Commonly used helpers.
# Autotools
def autotools_configure(ctx, extra_args = [], extra_env = {}):
env = {
@@ -8,15 +8,15 @@ def autotools_configure(ctx, extra_args = [], extra_env = {}):
}
env.update(extra_env)
ctx.run([
ctx.source_dir + "/configure",
ctx.source_dir / "configure",
"--host=" + options.target_triple,
"--with-sysroot=" + ctx.sysroot,
"--prefix=" + ctx.prefix,
"--sysconfdir=/etc",
"--localstatedir=/var",
"--bindir=" + ctx.prefix + "/bin",
"--sbindir=" + ctx.prefix + "/bin",
"--libdir=" + ctx.prefix + "/lib",
"--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)
@@ -25,7 +25,7 @@ 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})
ctx.run(["make", "install"] + extra_args, env = { "DESTDIR": pkg.dest_dir })
def autotools(configure_args = [], configure_env = {}, build_args = [], install_args = []):
def _configure(ctx):
@@ -35,3 +35,61 @@ def autotools(configure_args = [], configure_env = {}, build_args = [], install_
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()
+33 -7
View File
@@ -3,6 +3,7 @@ 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",
@@ -11,15 +12,40 @@ source = tarball_source(
)
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(
name = "limine-bios",
"limine-uefi",
meta(description = "UEFI files"),
[
"usr/share/limine/BOOT*.EFI",
"usr/share/limine/limine-uefi-*.bin",
],
),
]
configure, build, install = autotools(configure_env = {
"TOOLCHAIN_FOR_TARGET": options.target_triple + "-",
"LD_FOR_TARGET": options.target_triple + "-" + "ld",
"OBJCOPY_FOR_TARGET": options.target_triple + "-" + "objcopy",
"OBJDUMP_FOR_TARGET": options.target_triple + "-" + "objdump",
})
if options.target_arch == "x86_64":
subpackages += [
subpackage(
"limine-bios",
meta(description = "BIOS files"),
[
"usr/share/limine/limine-bios*",
],
)
]
+4 -4
View File
@@ -11,10 +11,10 @@ source = tarball_source(
)
def build(ctx):
ctx.run(["cp", "-rp", ctx.source_dir + "/.", ctx.build_dir])
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(["find", ctx.build_dir / "usr/include", "-type", "f", "!", "-name", "*.h", "-delete"])
def install(ctx, pkg):
ctx.run(["mkdir", "-p", pkg.dest_dir + ctx.prefix])
ctx.run(["cp", "-rp", ctx.build_dir + "/usr/include", pkg.dest_dir + ctx.prefix])
ctx.run(["mkdir", "-p", pkg.dest_dir / options.prefix])
ctx.run(["cp", "-rp", ctx.build_dir / "usr/include", pkg.dest_dir / options.prefix])
-37
View File
@@ -1,37 +0,0 @@
name = "linux"
version = "7.0.9"
revision = 1
description = "Linux kernel"
license = "GPL-2.0-only"
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):
result = [
"make",
"-C", ctx.source_dir,
"O=" + ctx.build_dir,
"ARCH=x86_64",
"CROSS_COMPILE=" + OPTIONS.target_triple + "-",
"-j" + str(ctx.jobs),
]
result.extend(args)
return result
def configure(ctx):
ctx.run(_make_args(ctx, "defconfig"))
def build(ctx):
ctx.run(_make_args(ctx, "bzImage"))
def install(ctx, pkg):
ctx.install(
ctx.build_dir + "/arch/x86/boot/bzImage",
pkg.destdir + "/boot/vmlinuz-" + version,
)
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",
])
+3 -2
View File
@@ -10,12 +10,13 @@ source = tarball_source(
strip_components = 1,
)
host_deps = ["binutils", "gcc-bootstrap"]
build_if = options.libc == "musl"
def configure(ctx):
ctx.run([
ctx.source_dir + "/configure",
ctx.source_dir / "configure",
"--target=" + options.target_triple,
"--prefix=" + ctx.prefix,
"--prefix=" + options.prefix,
"--syslibdir=/lib",
], env = {
"CC": options.target_triple + "-gcc",
+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})
+285 -47
View File
@@ -26,6 +26,36 @@ use crate::{
recipe::{GitSource, OutputPackage, Recipe, RecipeKind, RecipeSet, Source},
};
const SPLIT_SUBPACKAGE_SCRIPT: &str = r#"
base=$1
dest=$2
package=$3
shift 3
mkdir -p "$dest"
for pattern do
matches=$(mktemp)
find "$base" -depth -path "$base/$pattern" -print > "$matches"
if ! [ -s "$matches" ]; then
echo "subpackage $package: glob '$pattern' matched no files under $base" >&2
rm -f "$matches"
exit 1
fi
while IFS= read -r path; do
[ "$path" != "$base" ] || continue
rel=${path#"$base"/}
target=$dest/$rel
mkdir -p "$(dirname "$target")"
echo "$rel"
mv "$path" "$target"
done < "$matches"
rm -f "$matches"
done
find "$base" -depth -type d -empty -delete
"#;
#[derive(Debug)]
pub struct Builder {
root: PathBuf,
@@ -49,9 +79,10 @@ impl Builder {
rebuild: bool,
dry_run: bool,
) -> anyhow::Result<()> {
let requested = filter_skipped(recipes, requested);
let plan = TaskPlanner::new(&self.root, &self.config.arch, recipes)
.build_plan(requested, rebuild)?;
self.print_plan(&plan);
.build_plan(&requested, rebuild)?;
self.print_plan(recipes, &plan)?;
if dry_run {
return Ok(());
}
@@ -64,9 +95,10 @@ impl Builder {
requested: &[String],
dry_run: bool,
) -> anyhow::Result<()> {
let requested = filter_skipped(recipes, requested);
let plan =
TaskPlanner::new(&self.root, &self.config.arch, recipes).fetch_plan(requested)?;
self.print_plan(&plan);
TaskPlanner::new(&self.root, &self.config.arch, recipes).fetch_plan(&requested)?;
self.print_plan(recipes, &plan)?;
if dry_run {
return Ok(());
}
@@ -126,6 +158,11 @@ impl Builder {
let recipe = recipes.recipe(key)?;
self.task_prepare_sources(layout, recipe)
}
TaskId::PrepareRecipe(key) => {
let recipe = recipes.recipe(key)?;
let active = active.expect("recipe prepare task requires an active container");
self.task_prepare_recipe(layout, recipe, active)
}
TaskId::ConfigureRecipe(key) => {
let recipe = recipes.recipe(key)?;
let active = active.expect("configure task requires an active container");
@@ -204,7 +241,7 @@ impl Builder {
let mut tmp = tempfile::NamedTempFile::new_in(&cache_dir)
.with_context(|| format!("creating temp file in {}", cache_dir.display()))?;
let mut hasher = Sha256::new();
let mut buf = [0u8; 64 * 1024];
let mut buf: [u8; 65536] = [0u8; 64 * 1024];
loop {
let n = std::io::Read::read(&mut response, &mut buf)
.with_context(|| format!("reading response body for {url}"))?;
@@ -233,12 +270,6 @@ impl Builder {
})?;
}
if unknown {
bail!(
"{}: source sha256 is unknown; update the recipe with sha256 = \"{hash}\"",
recipe.key()
);
}
Ok(())
}
@@ -416,6 +447,22 @@ impl Builder {
Ok(())
}
fn task_prepare_recipe(
&self,
layout: &Layout<'_>,
recipe: &Recipe,
active: &ActiveContainer,
) -> anyhow::Result<()> {
let Some(func) = recipe.phases().prepare() else {
return Ok(());
};
log::step("prepare", &format!("{} (recipe phase)", recipe.key()));
let ctx = phase_context_for(recipe);
self.invoke_with_runtime(active, &[PhaseArg::Ctx(ctx)], func)?;
self.write_recipe_stamp(layout, recipe, "prepare")
}
fn task_configure(
&self,
layout: &Layout<'_>,
@@ -424,11 +471,7 @@ impl Builder {
) -> anyhow::Result<()> {
log::step("configure", &recipe.key());
if let Some(func) = recipe.phases().configure() {
let ctx = PhaseContext::new(
source_dir_for(recipe),
prefix_for(recipe.kind()),
default_jobs(),
);
let ctx = phase_context_for(recipe);
self.invoke_with_runtime(active, &[PhaseArg::Ctx(ctx)], func)?;
}
self.write_recipe_stamp(layout, recipe, "configure")
@@ -441,11 +484,7 @@ impl Builder {
active: &ActiveContainer,
) -> anyhow::Result<()> {
log::step("build", &recipe.key());
let ctx = PhaseContext::new(
source_dir_for(recipe),
prefix_for(recipe.kind()),
default_jobs(),
);
let ctx = phase_context_for(recipe);
self.invoke_with_runtime(active, &[PhaseArg::Ctx(ctx)], recipe.phases().build())?;
self.write_recipe_stamp(layout, recipe, "build")
}
@@ -458,17 +497,17 @@ impl Builder {
active: &ActiveContainer,
) -> anyhow::Result<()> {
log::step("install", &output.key());
if !output.is_base() {
return self.task_split_subpackage(layout, recipe, output, active);
}
let dest = format!("/dest/{}", output.name());
active.container.borrow().exec(
&["mkdir".to_owned(), "-p".to_owned(), dest.clone()],
&base_env(&active.base_path),
"/",
)?;
let ctx = PhaseContext::new(
source_dir_for(recipe),
prefix_for(recipe.kind()),
default_jobs(),
);
let ctx = phase_context_for(recipe);
let pkg = PackageContext::new(dest);
self.invoke_with_runtime(
active,
@@ -478,6 +517,36 @@ impl Builder {
self.write_output_stamp(layout, recipe, output, "install")
}
fn task_split_subpackage(
&self,
layout: &Layout<'_>,
recipe: &Recipe,
output: &OutputPackage,
active: &ActiveContainer,
) -> anyhow::Result<()> {
let base = recipe
.base_output()
.ok_or_else(|| anyhow::anyhow!("recipe `{}` has no base output", recipe.key()))?;
let base_dest = format!("/dest/{}", base.name());
let dest = format!("/dest/{}", output.name());
let mut argv: Vec<String> = vec![
"sh".to_owned(),
"-eu".to_owned(),
"-c".to_owned(),
SPLIT_SUBPACKAGE_SCRIPT.to_owned(),
"split-subpackage".to_owned(),
base_dest,
dest,
output.name().to_owned(),
];
argv.extend(output.file_globs().iter().cloned());
active
.container
.borrow()
.exec(&argv, &base_env(&active.base_path), "/")?;
self.write_output_stamp(layout, recipe, output, "install")
}
fn task_install_host(
&self,
layout: &Layout<'_>,
@@ -491,11 +560,7 @@ impl Builder {
&base_env(&active.base_path),
"/",
)?;
let ctx = PhaseContext::new(
source_dir_for(recipe),
prefix_for(recipe.kind()),
default_jobs(),
);
let ctx = phase_context_for(recipe);
let pkg = PackageContext::new(dest.clone());
self.invoke_with_runtime(
active,
@@ -595,6 +660,66 @@ impl Builder {
phase::invoke_phase(func, args)
}
fn populate_sysroot(
&self,
layout: &Layout<'_>,
recipes: &RecipeSet,
recipe: &Recipe,
active: &ActiveContainer,
deps: &[String],
) -> anyhow::Result<()> {
if deps.is_empty() {
return Ok(());
}
let env = base_env(&active.base_path);
log::step(
"sysroot",
&format!("{} from {} target apk(s)", recipe.key(), deps.len()),
);
for dep in deps {
let output = recipes.output(dep)?;
let owning = recipes.recipe(output.recipe())?;
let apk_host = layout.apk_path(owning, output);
if !apk_host.exists() {
bail!(
"missing apk for target dependency `{dep}` at {}; \
rebuild it first (e.g. `distro build -r {dep}`)",
apk_host.display()
);
}
let file_name = apk_host
.file_name()
.and_then(|n| n.to_str())
.ok_or_else(|| {
anyhow::anyhow!("apk path {} has no UTF-8 file name", apk_host.display())
})?;
active.container.borrow().exec(
&[
"apk".to_owned(),
"extract".to_owned(),
"--allow-untrusted".to_owned(),
"--force-overwrite".to_owned(),
"--destination".to_owned(),
"/sysroot".to_owned(),
format!("/pkgs/{file_name}"),
],
&env,
"/",
)?;
}
log::info(
"sysroot",
&format!(
"{}: extracted /sysroot from {} target apk(s)",
recipe.key(),
deps.len()
),
);
Ok(())
}
fn start_recipe_container(
&self,
recipes: &RecipeSet,
@@ -609,8 +734,10 @@ impl Builder {
fs::create_dir_all(&build_dir)?;
let host_deps = transitive_host_deps(recipes, recipe)?;
let target_deps = transitive_target_deps(recipes, recipe)?;
let pkgs_dir = self.root.join("build/pkgs").join(&self.config.arch);
fs::create_dir_all(&pkgs_dir)?;
let mut mounts = vec![
Mount {
host: source_dir,
@@ -628,6 +755,15 @@ impl Builder {
read_only: false,
},
];
if let Some(files_dir) = recipe.files_dir() {
mounts.push(Mount {
host: files_dir,
container: "/files".to_owned(),
read_only: true,
});
}
let mut tools_bins: Vec<String> = Vec::new();
for dep_key in &host_deps {
let dep_recipe = recipes.recipe(dep_key)?;
@@ -641,10 +777,10 @@ impl Builder {
let bare = dep_key.strip_prefix("host:").unwrap_or(dep_key);
mounts.push(Mount {
host: install,
container: format!("/tools/{bare}/usr/local"),
container: format!("/tools/{bare}"),
read_only: true,
});
tools_bins.push(format!("/tools/{bare}/usr/local/bin"));
tools_bins.push(format!("/tools/{bare}/bin"));
}
let name = format!(
@@ -674,6 +810,8 @@ impl Builder {
base_path,
};
self.populate_sysroot(&layout, recipes, recipe, &active, &target_deps)?;
Ok(active)
}
@@ -783,23 +921,54 @@ impl Builder {
Ok(hex::encode(hasher.finalize()))
}
fn print_plan(&self, plan: &TaskPlan) {
fn print_plan(&self, recipes: &RecipeSet, plan: &TaskPlan) -> anyhow::Result<()> {
if plan.is_empty() {
log::skip("plan", "nothing to do");
return;
return Ok(());
}
let task_count = plan.order().len();
let edge_count = plan.dependency_count();
log::step(
"plan",
&format!(
"{} active task(s), {} edge(s)",
plan.order().len(),
plan.dependency_count()
"{} {}, {} {}",
task_count,
plural(task_count, "task", "tasks"),
edge_count,
plural(edge_count, "dependency edge", "dependency edges")
),
);
for task in plan.order() {
println!("{task}");
let step_width = task_count.to_string().len() * 2 + 1;
let action_width = plan
.order()
.iter()
.map(|task| plan_task_parts(task).0.len())
.max()
.unwrap_or(0);
let mut current_recipe: Option<String> = None;
for (index, task) in plan.order().iter().enumerate() {
let recipe_slug = task_recipe_slug(task, recipes)?;
if current_recipe.as_deref() != Some(recipe_slug.as_str()) {
if current_recipe.is_some() {
println!();
}
println!(" {recipe_slug}");
current_recipe = Some(recipe_slug.clone());
}
let step = format!("{}/{}", index + 1, task_count);
let (action, target) = plan_task_parts(task);
if target == recipe_slug {
println!(" {step:>step_width$} {action}");
} else {
println!(" {step:>step_width$} {action:<action_width$} {target}");
}
}
Ok(())
}
fn abs_config_path(&self, path: &Path) -> PathBuf {
@@ -817,6 +986,14 @@ struct ActiveContainer {
base_path: String,
}
fn filter_skipped(recipes: &RecipeSet, requested: &[String]) -> Vec<String> {
requested
.iter()
.filter(|key| !recipes.is_skipped(key))
.cloned()
.collect()
}
fn task_needs_container(task: &TaskId) -> bool {
matches!(
task,
@@ -825,16 +1002,32 @@ fn task_needs_container(task: &TaskId) -> bool {
| TaskId::InstallPackageFiles(_)
| TaskId::ProduceApk(_)
| TaskId::InstallHostRecipe(_)
| TaskId::PrepareRecipe(_)
)
}
fn prefix_for(kind: RecipeKind) -> &'static str {
match kind {
RecipeKind::Package => "/usr",
RecipeKind::HostPackage => "/usr/local",
fn plan_task_parts(task: &TaskId) -> (&'static str, &str) {
match task {
TaskId::FetchSources(recipe) => ("fetch sources", recipe),
TaskId::PrepareSources(recipe) => ("prepare sources", recipe),
TaskId::PrepareRecipe(recipe) => ("recipe prepare", recipe),
TaskId::ConfigureRecipe(recipe) => ("configure", recipe),
TaskId::BuildRecipe(recipe) => ("build", recipe),
TaskId::InstallPackageFiles(output) => ("install files", output),
TaskId::ProduceApk(output) => ("produce apk", output),
TaskId::InstallHostRecipe(recipe) => ("install host", recipe),
}
}
fn plural<'a>(count: usize, singular: &'a str, plural: &'a str) -> &'a str {
if count == 1 { singular } else { plural }
}
fn phase_context_for(recipe: &Recipe) -> PhaseContext {
let files = recipe.files_dir().map(|_| "/files".to_owned());
PhaseContext::new(source_dir_for(recipe), default_jobs(), files)
}
fn source_dir_for(recipe: &Recipe) -> SourceDir {
let entries = recipe.sources().entries();
let named: Vec<(&str, &crate::recipe::Source)> = entries
@@ -858,13 +1051,30 @@ fn default_jobs() -> i32 {
.unwrap_or(1)
}
const TERMINAL_ENV_VARS: &[&str] = &[
"TERM",
"COLORTERM",
"NO_COLOR",
"CLICOLOR",
"CLICOLOR_FORCE",
"FORCE_COLOR",
"TERM_PROGRAM",
"TERM_PROGRAM_VERSION",
];
fn bare_env() -> Vec<(String, String)> {
vec![
let mut env = vec![
("PATH".to_owned(), String::new()),
("HOME".to_owned(), "/tmp".to_owned()),
("TERM".to_owned(), "dumb".to_owned()),
("LC_ALL".to_owned(), "C".to_owned()),
]
];
env.extend(TERMINAL_ENV_VARS.iter().filter_map(|key| {
std::env::var(key)
.ok()
.filter(|value| !value.is_empty())
.map(|value| ((*key).to_owned(), value))
}));
env
}
fn base_env(path: &str) -> Vec<(String, String)> {
@@ -896,6 +1106,34 @@ fn transitive_host_deps(recipes: &RecipeSet, recipe: &Recipe) -> anyhow::Result<
Ok(order)
}
/// Compute the transitive closure of a recipe's target package dependencies.
/// The recipe's own `build_deps` and `deps` seed the queue; for each visited
/// dependency we follow its `deps` (runtime+build-needed) recursively, but
/// not its `build_deps` (build-time-only relative to *that* package, hence
/// not propagated outward).
fn transitive_target_deps(recipes: &RecipeSet, recipe: &Recipe) -> anyhow::Result<Vec<String>> {
let mut order: Vec<String> = Vec::new();
let mut seen: BTreeSet<String> = BTreeSet::new();
let mut queue: VecDeque<String> = recipe
.build_deps()
.iter()
.chain(recipe.deps().iter())
.cloned()
.collect();
while let Some(dep) = queue.pop_front() {
if !seen.insert(dep.clone()) {
continue;
}
let output = recipes.output(&dep)?;
let owning = recipes.recipe(output.recipe())?;
for sub in owning.deps() {
queue.push_back(sub.clone());
}
order.push(dep);
}
Ok(order)
}
fn random_suffix() -> u64 {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
+152 -23
View File
@@ -1,6 +1,15 @@
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};
@@ -21,6 +30,41 @@ pub struct Container {
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,
@@ -77,6 +121,8 @@ impl Container {
bail!("`{runtime_str} run` returned an empty container id");
}
register_container(runtime_str, &id);
Ok(Self {
runtime: runtime_str,
id,
@@ -123,29 +169,6 @@ impl Container {
}
Ok(())
}
pub fn stop(mut self) -> anyhow::Result<()> {
self.stop_inner()
}
fn stop_inner(&mut self) -> anyhow::Result<()> {
if self.stopped {
return Ok(());
}
self.stopped = true;
let status = Command::new(self.runtime)
.arg("rm")
.arg("-f")
.arg(&self.id)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.with_context(|| format!("spawning `{} rm`", self.runtime))?;
if !status.success() {
bail!("`{} rm -f {}` failed with {status}", self.runtime, self.id);
}
Ok(())
}
}
impl Drop for Container {
@@ -158,6 +181,112 @@ impl Drop for Container {
.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);
}
}
+24 -8
View File
@@ -3,13 +3,14 @@ use starlark::{
environment::{FrozenModule, Globals, GlobalsBuilder, Module},
eval::Evaluator,
syntax::{AstModule, Dialect},
values::list::ListRef,
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},
};
@@ -45,23 +46,38 @@ fn builder_globals(builder: &mut GlobalsBuilder) {
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) -> anyhow::Result<Source> {
Ok(Source::Git(GitSource::new(url, commit)))
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: Option<&Metadata>) -> anyhow::Result<Subpackage> {
let metadata = metadata
.cloned()
.unwrap_or_else(|| Metadata::new(None, None, None, None));
Ok(Subpackage::new(name, metadata))
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))
}
}
+65 -16
View File
@@ -15,6 +15,7 @@ use crate::{
pub enum TaskId {
FetchSources(String),
PrepareSources(String),
PrepareRecipe(String),
ConfigureRecipe(String),
BuildRecipe(String),
InstallPackageFiles(String),
@@ -27,6 +28,7 @@ impl fmt::Display for TaskId {
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}"),
@@ -54,17 +56,12 @@ impl TaskPlan {
pub fn dependency_count(&self) -> usize {
self.dependencies.values().map(Vec::len).sum()
}
#[cfg(test)]
pub fn dependencies(&self, task: &TaskId) -> Option<&[TaskId]> {
self.dependencies.get(task).map(Vec::as_slice)
}
}
pub struct TaskPlanner<'a> {
layout: Layout<'a>,
recipes: &'a RecipeSet,
force: bool,
forced_recipes: BTreeSet<String>,
dependencies: BTreeMap<TaskId, Vec<TaskId>>,
inactive: BTreeSet<TaskId>,
visiting: BTreeSet<TaskId>,
@@ -76,7 +73,7 @@ impl<'a> TaskPlanner<'a> {
Self {
layout: Layout::new(root, arch),
recipes,
force: false,
forced_recipes: BTreeSet::new(),
dependencies: BTreeMap::new(),
inactive: BTreeSet::new(),
visiting: BTreeSet::new(),
@@ -85,9 +82,11 @@ impl<'a> TaskPlanner<'a> {
}
pub fn build_plan(mut self, requests: &[String], force: bool) -> anyhow::Result<TaskPlan> {
self.force = force;
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() {
@@ -149,9 +148,13 @@ impl<'a> TaskPlanner<'a> {
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())];
let mut deps = vec![
TaskId::PrepareSources(recipe.key()),
TaskId::PrepareRecipe(recipe.key()),
];
deps.extend(
recipe
.host_deps()
@@ -170,12 +173,29 @@ impl<'a> TaskPlanner<'a> {
TaskId::BuildRecipe(recipe) => Ok(vec![TaskId::ConfigureRecipe(recipe.clone())]),
TaskId::InstallPackageFiles(output) => {
let output = self.recipes.output(output)?;
Ok(vec![TaskId::BuildRecipe(output.recipe().to_owned())])
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()
@@ -198,6 +218,9 @@ impl<'a> TaskPlanner<'a> {
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")
}
@@ -207,8 +230,19 @@ impl<'a> TaskPlanner<'a> {
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, output)?
|| self.produce_apk_active(recipe, base)?)
}
TaskId::ProduceApk(output) => {
let output = self.recipes.output(output)?;
@@ -229,8 +263,12 @@ impl<'a> TaskPlanner<'a> {
}))
}
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.force {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
let want_version = format!("{}-r{}", recipe.version(), recipe.revision());
@@ -249,8 +287,18 @@ impl<'a> TaskPlanner<'a> {
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.force {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
Ok(
@@ -267,7 +315,7 @@ impl<'a> TaskPlanner<'a> {
output: &OutputPackage,
kind: &str,
) -> anyhow::Result<bool> {
if self.force {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
Ok(
@@ -279,7 +327,7 @@ impl<'a> TaskPlanner<'a> {
}
fn produce_apk_active(&self, recipe: &Recipe, output: &OutputPackage) -> anyhow::Result<bool> {
if self.force {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
if !self.layout.apk_path(recipe, output).exists() {
@@ -294,7 +342,7 @@ impl<'a> TaskPlanner<'a> {
}
fn install_host_recipe_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
if self.force {
if self.is_recipe_forced(recipe) {
return Ok(true);
}
if !self.layout.host_install_dir(recipe).exists() {
@@ -375,6 +423,7 @@ pub fn task_recipe_slug(task: &TaskId, recipes: &RecipeSet) -> anyhow::Result<St
Ok(match task {
TaskId::FetchSources(recipe)
| TaskId::PrepareSources(recipe)
| TaskId::PrepareRecipe(recipe)
| TaskId::ConfigureRecipe(recipe)
| TaskId::BuildRecipe(recipe)
| TaskId::InstallHostRecipe(recipe) => recipe.clone(),
+9 -17
View File
@@ -35,10 +35,7 @@ impl<'a> Layout<'a> {
}
pub fn host_install_dir(&self, recipe: &Recipe) -> PathBuf {
self.root
.join("build/host-pkgs")
.join(recipe.slug())
.join("usr/local")
self.root.join("build/host-pkgs").join(recipe.slug())
}
pub fn host_install_root(&self, recipe: &Recipe) -> PathBuf {
@@ -111,21 +108,16 @@ impl<'a> Layout<'a> {
}
pub fn recipe_patches(&self, recipe: &Recipe) -> anyhow::Result<Vec<PathBuf>> {
let patches_dir = recipe.dir().join("patches");
if !patches_dir.exists() {
let Some(data_dir) = recipe.data_dir() else {
return Ok(Vec::new());
}
let mut patches = Vec::new();
for entry in fs::read_dir(&patches_dir)
.with_context(|| format!("reading patches directory {}", patches_dir.display()))?
{
let entry = entry?;
let path = entry.path();
if path.is_file() {
patches.push(path);
};
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));
}
}
patches.sort();
Ok(patches)
Ok(out)
}
}
+1
View File
@@ -11,5 +11,6 @@ mod phase;
mod recipe;
fn main() -> anyhow::Result<()> {
container::install_signals()?;
cli::run()
}
+120 -27
View File
@@ -6,7 +6,9 @@ use starlark::{
collections::SmallMap,
environment::{Methods, MethodsBuilder, MethodsStatic, Module},
eval::Evaluator,
values::{Heap, OwnedFrozenValue, StarlarkValue, Value, list::UnpackList, none::NoneType},
values::{
Heap, OwnedFrozenValue, StarlarkValue, Value, ValueLike, list::UnpackList, none::NoneType,
},
};
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_module, starlark_value};
@@ -53,6 +55,78 @@ fn with_current<R>(f: impl FnOnce(&PhaseRuntime) -> R) -> anyhow::Result<R> {
})
}
#[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,
@@ -126,24 +200,29 @@ impl<'v> StarlarkValue<'v> for SourceDir {
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,
prefix: String,
sysroot: String,
files: Option<String>,
jobs: i32,
}
impl PhaseContext {
pub fn new(source_dir: SourceDir, prefix: &str, jobs: i32) -> Self {
pub fn new(source_dir: SourceDir, jobs: i32, files: Option<String>) -> Self {
Self {
source_dir,
build_dir: "/build".to_owned(),
prefix: prefix.to_owned(),
sysroot: "/sysroot".to_owned(),
files,
jobs,
}
}
@@ -162,33 +241,34 @@ 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(self.build_dir.as_str()),
"prefix" => heap.alloc(self.prefix.as_str()),
"sysroot" => heap.alloc(self.sysroot.as_str()),
"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 {
matches!(
attr,
"source_dir" | "build_dir" | "prefix" | "sysroot" | "jobs" | "run"
)
match attr {
"source_dir" | "build_dir" | "sysroot" | "jobs" | "run" => true,
"files" => self.files.is_some(),
_ => false,
}
}
fn dir_attr(&self) -> Vec<String> {
[
"source_dir",
"build_dir",
"prefix",
"sysroot",
"jobs",
"run",
]
.into_iter()
.map(String::from)
.collect()
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> {
@@ -201,10 +281,22 @@ impl<'v> StarlarkValue<'v> for PhaseContext {
fn phase_context_methods(builder: &mut MethodsBuilder) {
fn run<'v>(
#[starlark(this)] _this: Value<'v>,
#[starlark(require = pos)] argv: UnpackList<String>,
#[starlark(require = named)] env: Option<SmallMap<String, String>>,
#[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> {
run_in_container(&argv.items, env.unwrap_or_default())?;
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)
}
}
@@ -212,6 +304,7 @@ fn phase_context_methods(builder: &mut MethodsBuilder) {
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
@@ -235,7 +328,7 @@ fn run_in_container(
}
(runtime.container.clone(), env)
})?;
container.borrow().exec(argv, &env, "/build")
container.borrow().exec(argv, &env, cwd)
}
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
@@ -261,7 +354,7 @@ starlark::starlark_simple_value!(PackageContext);
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(self.dest_dir.as_str())),
"dest_dir" => Some(heap.alloc(Path::new(self.dest_dir.clone()))),
_ => None,
}
}
+17
View File
@@ -51,4 +51,21 @@ impl Metadata {
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(),
}
}
}
+140 -69
View File
@@ -5,14 +5,14 @@ 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,
fmt,
collections::{HashMap, HashSet},
path::{Path, PathBuf},
};
use walkdir::WalkDir;
@@ -98,24 +98,6 @@ pub struct Recipe {
phases: RecipePhases,
}
impl fmt::Debug for Recipe {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Recipe")
.field("path", &self.path)
.field("kind", &self.kind)
.field("version", &self.version)
.field("revision", &self.revision)
.field("sources", &self.sources)
.field("outputs", &self.outputs)
.field("host_deps", &self.host_deps)
.field("build_deps", &self.build_deps)
.field("deps", &self.deps)
.field("run_deps", &self.run_deps)
.field("phases", &self.phases)
.finish()
}
}
impl Recipe {
pub fn load(
path: &Path,
@@ -123,10 +105,14 @@ impl Recipe {
kind: RecipeKind,
options: &Options,
lib: Option<&FrozenModule>,
) -> anyhow::Result<Self> {
) -> 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}"))?;
@@ -170,7 +156,7 @@ impl Recipe {
let key_str = key
.unpack_str()
.ok_or_else(|| anyhow::anyhow!("field `sources`: keys must be strings"))?;
if !is_valid_source_name(key_str) {
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)"
);
@@ -186,10 +172,10 @@ impl Recipe {
}
};
let host_deps = optional_string_list(&module, "host_deps")?;
let build_deps = optional_string_list(&module, "build_deps")?;
let deps = optional_string_list(&module, "deps")?;
let run_deps = optional_string_list(&module, "run_deps")?;
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 {
@@ -198,6 +184,8 @@ impl Recipe {
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)
@@ -208,10 +196,13 @@ impl Recipe {
"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().clone(),
metadata: sub.metadata().inherit(&metadata),
file_globs: sub.file_globs().to_vec(),
base: false,
});
}
}
@@ -230,7 +221,7 @@ impl Recipe {
.map_err(|err| anyhow::anyhow!("freezing recipe module {}: {err:?}", path.display()))?;
let phases = RecipePhases::load(&module)?;
Ok(Recipe {
let recipe = Recipe {
name: name.to_owned(),
path: path.to_path_buf(),
kind,
@@ -243,7 +234,8 @@ impl Recipe {
deps,
run_deps,
phases,
})
};
Ok(Some(recipe))
}
pub fn phases(&self) -> &RecipePhases {
@@ -258,8 +250,21 @@ impl Recipe {
self.kind.slug(&self.name)
}
pub fn dir(&self) -> &Path {
self.path.parent().unwrap_or_else(|| Path::new("."))
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 {
@@ -286,6 +291,10 @@ impl Recipe {
&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
}
@@ -301,52 +310,94 @@ impl Recipe {
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}")),
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(())
}
}
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 == '_')
}
pub struct RecipePhases {
prepare: Option<OwnedFrozenValue>,
configure: Option<OwnedFrozenValue>,
build: OwnedFrozenValue,
install: OwnedFrozenValue,
}
impl fmt::Debug for RecipePhases {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RecipePhases")
.field("configure", &self.configure.is_some())
.field("build", &true)
.field("install", &true)
.finish()
}
}
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()
}
@@ -393,7 +444,7 @@ fn validate_phase_function(name: &str, value: &OwnedFrozenValue) -> anyhow::Resu
Ok(())
}
#[derive(Clone, Debug)]
#[derive(Clone)]
pub struct OutputPackage {
/// Canonical key of the owning recipe.
recipe: String,
@@ -401,6 +452,10 @@ pub struct OutputPackage {
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 {
@@ -419,12 +474,20 @@ impl OutputPackage {
pub fn metadata(&self) -> &Metadata {
&self.metadata
}
pub fn file_globs(&self) -> &[String] {
&self.file_globs
}
pub fn is_base(&self) -> bool {
self.base
}
}
#[derive(Debug)]
pub struct RecipeSet {
recipes: HashMap<String, Recipe>,
outputs: HashMap<String, OutputPackage>,
skipped: HashSet<String>,
}
impl RecipeSet {
@@ -435,6 +498,7 @@ impl RecipeSet {
) -> 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),
@@ -447,9 +511,13 @@ impl RecipeSet {
}
for (name, path) in discover_recipes(&recipes_dir)? {
let recipe = Recipe::load(&path, &name, kind, options, lib)
.with_context(|| format!("loading recipe `{name}`"))?;
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}`");
@@ -467,7 +535,11 @@ impl RecipeSet {
}
}
Ok(Self { recipes, outputs })
Ok(Self {
recipes,
outputs,
skipped,
})
}
pub fn recipe(&self, key: &str) -> anyhow::Result<&Recipe> {
@@ -481,13 +553,12 @@ impl RecipeSet {
.get(key)
.ok_or_else(|| anyhow::anyhow!("unknown output package `{key}`"))
}
pub fn is_skipped(&self, key: &str) -> bool {
self.skipped.contains(key)
}
}
/// Find all recipe `.star` files under `dir`, returning a map of recipe name
/// to its `.star` file. Intermediate directories act as categories and are
/// not themselves recipes. Within any subtree, a recipe takes either form:
/// - `.../<name>.star` — name is the file stem
/// - `.../<name>/recipe.star` — name is the parent directory
fn discover_recipes(dir: &Path) -> anyhow::Result<HashMap<String, PathBuf>> {
let mut recipes: HashMap<String, PathBuf> = HashMap::new();
+25 -3
View File
@@ -7,6 +7,7 @@ pub struct TarballSource {
url: String,
sha256: String,
strip_components: u32,
patches: Vec<String>,
}
impl std::fmt::Display for TarballSource {
@@ -21,11 +22,12 @@ starlark::starlark_simple_value!(TarballSource);
impl<'v> StarlarkValue<'v> for TarballSource {}
impl TarballSource {
pub fn new(url: String, sha256: String, strip_components: u32) -> Self {
pub fn new(url: String, sha256: String, strip_components: u32, patches: Vec<String>) -> Self {
Self {
url,
sha256,
strip_components,
patches,
}
}
@@ -40,12 +42,17 @@ impl TarballSource {
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 {
@@ -60,8 +67,12 @@ starlark::starlark_simple_value!(GitSource);
impl<'v> StarlarkValue<'v> for GitSource {}
impl GitSource {
pub fn new(url: String, commit: String) -> Self {
Self { url, commit }
pub fn new(url: String, commit: String, patches: Vec<String>) -> Self {
Self {
url,
commit,
patches,
}
}
pub fn url(&self) -> &str {
@@ -71,6 +82,10 @@ impl GitSource {
pub fn commit(&self) -> &str {
&self.commit
}
pub fn patches(&self) -> &[String] {
&self.patches
}
}
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
@@ -108,4 +123,11 @@ impl Source {
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(),
}
}
}
+11 -2
View File
@@ -7,18 +7,27 @@ 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, metadata: Metadata) -> Self {
Self { name, metadata }
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
}