Compare commits
6 Commits
main
..
fc97bc4bb2
| Author | SHA1 | Date | |
|---|---|---|---|
| fc97bc4bb2 | |||
| 3253dfe87b | |||
| b6e18c474e | |||
| 2e6704516a | |||
| 1a7c817fb9 | |||
| a525868969 |
@@ -1,4 +0,0 @@
|
|||||||
target
|
|
||||||
build
|
|
||||||
.git
|
|
||||||
*.bak
|
|
||||||
+5
-4
@@ -1,4 +1,5 @@
|
|||||||
/target
|
__pycache__/
|
||||||
/build
|
/cache
|
||||||
*.bak
|
/build*
|
||||||
*.lock
|
/sources
|
||||||
|
/sysroot
|
||||||
|
|||||||
Generated
-2900
File diff suppressed because it is too large
Load Diff
-23
@@ -1,23 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "distro"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = "1.0"
|
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
|
||||||
hex = "0.4"
|
|
||||||
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"
|
|
||||||
+51
-47
@@ -1,58 +1,62 @@
|
|||||||
FROM docker.io/library/alpine:edge
|
FROM alpine:edge
|
||||||
|
|
||||||
RUN apk upgrade --no-cache && \
|
RUN mkdir -p /sources /build /pkgs /sysroot /dest /tools /files
|
||||||
apk add --no-cache \
|
|
||||||
alpine-sdk \
|
RUN apk add --no-cache \
|
||||||
apk-tools \
|
apk-tools \
|
||||||
autoconf \
|
build-base \
|
||||||
automake \
|
|
||||||
bash \
|
bash \
|
||||||
bc \
|
patch \
|
||||||
bison \
|
tar \
|
||||||
bzip2 \
|
xz \
|
||||||
ca-certificates \
|
zstd \
|
||||||
cmake \
|
|
||||||
coreutils \
|
|
||||||
curl \
|
|
||||||
file \
|
file \
|
||||||
findutils \
|
findutils \
|
||||||
flex \
|
coreutils \
|
||||||
gettext-dev \
|
diffutils \
|
||||||
git \
|
grep \
|
||||||
gzip \
|
sed \
|
||||||
elfutils-dev \
|
gawk \
|
||||||
|
musl-dev \
|
||||||
|
linux-headers \
|
||||||
gmp-dev \
|
gmp-dev \
|
||||||
mpfr-dev \
|
mpfr-dev \
|
||||||
mpc1-dev \
|
mpc1-dev \
|
||||||
libtool \
|
isl-dev \
|
||||||
linux-headers \
|
zlib-dev \
|
||||||
meson \
|
git \
|
||||||
ninja \
|
|
||||||
openssl \
|
|
||||||
openssl-dev \
|
|
||||||
patch \
|
|
||||||
pkgconf \
|
pkgconf \
|
||||||
|
patchelf \
|
||||||
|
gperf \
|
||||||
python3 \
|
python3 \
|
||||||
tar \
|
python3-dev \
|
||||||
texinfo \
|
py3-mako \
|
||||||
xz \
|
py3-yaml \
|
||||||
zstd
|
py3-packaging \
|
||||||
|
py3-docutils \
|
||||||
|
py3-passlib \
|
||||||
|
perl \
|
||||||
|
m4 \
|
||||||
|
libtool \
|
||||||
|
gettext-dev \
|
||||||
|
bison \
|
||||||
|
flex \
|
||||||
|
which \
|
||||||
|
ca-certificates \
|
||||||
|
rsync \
|
||||||
|
mtools \
|
||||||
|
nasm \
|
||||||
|
cmake \
|
||||||
|
ninja \
|
||||||
|
meson \
|
||||||
|
glslang \
|
||||||
|
elfutils-dev \
|
||||||
|
libffi-dev \
|
||||||
|
expat-dev \
|
||||||
|
libxml2-dev \
|
||||||
|
pcre2-dev \
|
||||||
|
openssl-dev \
|
||||||
|
openssl \
|
||||||
|
ncurses
|
||||||
|
|
||||||
RUN rm -rf /tmp/mkpkg-root /tmp/distro-preflight.apk /tmp/APKINDEX.adb /tmp/distro-preflight.rsa && \
|
WORKDIR /build
|
||||||
openssl genrsa -out /tmp/distro-preflight.rsa 2048 >/dev/null 2>&1 && \
|
|
||||||
openssl rsa -in /tmp/distro-preflight.rsa -pubout -out /etc/apk/keys/distro-preflight.rsa.pub >/dev/null 2>&1 && \
|
|
||||||
mkdir -p /tmp/mkpkg-root/usr/share/distro && \
|
|
||||||
printf ok > /tmp/mkpkg-root/usr/share/distro/preflight && \
|
|
||||||
apk --sign-key /tmp/distro-preflight.rsa mkpkg \
|
|
||||||
--files /tmp/mkpkg-root \
|
|
||||||
--output /tmp/distro-preflight.apk \
|
|
||||||
--info name:distro-preflight \
|
|
||||||
--info version:0-r0 \
|
|
||||||
--info arch:noarch \
|
|
||||||
--info description:preflight \
|
|
||||||
--info license:MIT >/dev/null && \
|
|
||||||
apk --sign-key /tmp/distro-preflight.rsa mkndx -o /tmp/APKINDEX.adb /tmp/distro-preflight.apk >/dev/null && \
|
|
||||||
test -s /tmp/distro-preflight.apk && \
|
|
||||||
test -s /tmp/APKINDEX.adb
|
|
||||||
|
|
||||||
WORKDIR /work
|
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
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.
|
|
||||||
-36
@@ -1,36 +0,0 @@
|
|||||||
container_runtime = "podman"
|
|
||||||
container_image = "localhost/distro-builder:latest"
|
|
||||||
container_dockerfile = "Dockerfile"
|
|
||||||
|
|
||||||
signing_key = "build/keys/distro.rsa"
|
|
||||||
signing_pubkey = "build/keys/distro.rsa.pub"
|
|
||||||
|
|
||||||
target_arch = "x86_64"
|
|
||||||
libc = "musl"
|
|
||||||
|
|
||||||
host_cflags = "-O2 -pipe"
|
|
||||||
host_cxxflags = ""
|
|
||||||
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"
|
|
||||||
|
|
||||||
if target_arch == "x86_64":
|
|
||||||
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"
|
|
||||||
|
|
||||||
options = {
|
|
||||||
"libc": libc,
|
|
||||||
"target_triple": target_arch + "-linux-" + libc,
|
|
||||||
"host_cflags": host_cflags,
|
|
||||||
"host_cxxflags": host_cxxflags,
|
|
||||||
"host_ldflags": host_ldflags,
|
|
||||||
"cflags": target_cflags,
|
|
||||||
"cxxflags": target_cxxflags,
|
|
||||||
"ldflags": target_ldflags,
|
|
||||||
"wayland": True,
|
|
||||||
"x11": True,
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
version = "2.72"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU autoconf"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
url = "https://www.gnu.org/software/autoconf/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/autoconf/autoconf-{version}.tar.xz",
|
||||||
|
sha256="ba885c1319578d6c94d46e9b0dceb4014caafe2490e437a0dbca3f270a223f5a",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run(self.source_dir / "configure", f"--prefix={self.prefix}")
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
autotools_build(self)
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
autotools_install(self)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
version = "1.17"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU automake"
|
||||||
|
license = "GPL-2.0-or-later"
|
||||||
|
url = "https://www.gnu.org/software/automake/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/automake/automake-{version}.tar.xz",
|
||||||
|
sha256="8920c1fc411e13b90bf704ef9db6f29d540e76d232cb3b2c9f4dc4cc599bd990",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf"]
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run(self.source_dir / "configure", f"--prefix={self.prefix}")
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
autotools_build(self)
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
autotools_install(self)
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
version = "2.46.0"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU binutils cross-compiled for the target triple"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/binutils/binutils-{version}.tar.xz",
|
||||||
|
sha256="d75a94f4d73e7a4086f7513e67e439e8fcdcbb726ffe63f4661744e6256b2cf2",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run(
|
||||||
|
self.source_dir / "configure",
|
||||||
|
f"--prefix={self.prefix}",
|
||||||
|
f"--target={self.triple}",
|
||||||
|
f"--with-sysroot={self.sysroot}",
|
||||||
|
"--with-pic",
|
||||||
|
"--enable-cet",
|
||||||
|
"--enable-default-execstack=no",
|
||||||
|
"--enable-deterministic-archives",
|
||||||
|
"--enable-ld=default",
|
||||||
|
"--enable-new-dtags",
|
||||||
|
"--enable-plugins",
|
||||||
|
"--enable-relro",
|
||||||
|
"--enable-separate-code",
|
||||||
|
"--enable-threads",
|
||||||
|
"--disable-nls",
|
||||||
|
"--disable-werror",
|
||||||
|
# gprofng's libcollector relies on glibc-specific internals.
|
||||||
|
"--disable-gprofng",
|
||||||
|
env={
|
||||||
|
"CFLAGS": self.profile["host_cflags"],
|
||||||
|
"CXXFLAGS": self.profile["host_cxxflags"],
|
||||||
|
"LDFLAGS": self.profile["host_ldflags"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_, build, install = autotools()
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
name = "binutils"
|
|
||||||
version = "2.46.0"
|
|
||||||
revision = 1
|
|
||||||
description = "GNU binutils cross-compiled for the target triple"
|
|
||||||
license = "GPL-3.0-or-later"
|
|
||||||
|
|
||||||
source = {
|
|
||||||
"url": "https://ftp.gnu.org/gnu/binutils/binutils-" + version + ".tar.xz",
|
|
||||||
"sha256": "d75a94f4d73e7a4086f7513e67e439e8fcdcbb726ffe63f4661744e6256b2cf2",
|
|
||||||
"strip_components": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
host_deps = []
|
|
||||||
|
|
||||||
def configure(ctx):
|
|
||||||
ctx.run([
|
|
||||||
ctx.source_dir + "/configure",
|
|
||||||
"--prefix=" + ctx.prefix,
|
|
||||||
"--target=" + OPTIONS.target_triple,
|
|
||||||
"--with-sysroot=" + ctx.prefix + "/" + OPTIONS.target_triple,
|
|
||||||
"--disable-nls",
|
|
||||||
"--disable-werror",
|
|
||||||
"--enable-deterministic-archives",
|
|
||||||
"--enable-ld=default",
|
|
||||||
"--enable-plugins",
|
|
||||||
"--enable-threads",
|
|
||||||
"--with-system-zlib",
|
|
||||||
# gprofng's libcollector does not build against musl/recent gcc.
|
|
||||||
"--disable-gprofng",
|
|
||||||
], 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", "DESTDIR=" + pkg.destdir, "install"])
|
|
||||||
# Drop static archives we don't need on the cross side.
|
|
||||||
ctx.run(["sh", "-c", "rm -f " + pkg.destdir + ctx.prefix + "/lib/*.a"])
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
version = "16.1.0"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
|
||||||
|
sha256="50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
|
||||||
|
)
|
||||||
|
host_deps = ["binutils"]
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run(
|
||||||
|
self.source_dir / "configure",
|
||||||
|
f"--target={self.triple}",
|
||||||
|
f"--prefix={self.prefix}",
|
||||||
|
f"--with-sysroot={self.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": self.profile["host_cflags"],
|
||||||
|
"CXXFLAGS": self.profile["host_cxxflags"],
|
||||||
|
"LDFLAGS": self.profile["host_ldflags"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
self.run("make", f"-j{self.jobs}", "all-gcc")
|
||||||
|
self.run("make", f"-j{self.jobs}", "all-target-libgcc")
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
self.run("make", "install-gcc", env={"DESTDIR": str(self.dest_dir)})
|
||||||
|
self.run("make", "install-target-libgcc", env={"DESTDIR": str(self.dest_dir)})
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
version = "16.1.0"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU GCC cross-compiler targeting the system triple"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
url = "https://gcc.gnu.org/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
|
||||||
|
sha256="50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
|
||||||
|
)
|
||||||
|
host_deps = ["binutils", "gcc-bootstrap"]
|
||||||
|
deps = [profile["libc"], "linux-headers"]
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run(
|
||||||
|
self.source_dir / "configure",
|
||||||
|
f"--target={self.triple}",
|
||||||
|
f"--prefix={self.prefix}",
|
||||||
|
f"--with-sysroot={self.sysroot}",
|
||||||
|
f"--with-build-sysroot={self.sysroot}",
|
||||||
|
f"--with-gxx-include-dir={self.sysroot}{self.profile['includedir']}/c++/{version}",
|
||||||
|
"--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": self.profile["host_cflags"],
|
||||||
|
"CXXFLAGS": self.profile["host_cxxflags"],
|
||||||
|
"LDFLAGS": self.profile["host_ldflags"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
self.run("make", f"-j{self.jobs}")
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
self.run("make", "install-strip", env={"DESTDIR": str(self.dest_dir)})
|
||||||
|
# Drop libtool archives.
|
||||||
|
self.run("find", self.dest_dir, "-name", "*.la", "-delete")
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
name = "gcc"
|
|
||||||
version = "16.1.0"
|
|
||||||
revision = 1
|
|
||||||
description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)"
|
|
||||||
license = "GPL-3.0-or-later"
|
|
||||||
|
|
||||||
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",
|
|
||||||
"--prefix=" + ctx.prefix,
|
|
||||||
"--target=" + OPTIONS.target_triple,
|
|
||||||
"--with-sysroot=" + ctx.prefix + "/" + OPTIONS.target_triple,
|
|
||||||
"--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", "DESTDIR=" + pkg.destdir, "install-gcc"])
|
|
||||||
ctx.run(["make", "DESTDIR=" + pkg.destdir, "install-target-libgcc"])
|
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
version = "22.1.6"
|
||||||
|
revision = 1
|
||||||
|
description = "LLVM compiler infrastructure with clang and lld"
|
||||||
|
license = "Apache-2.0 WITH LLVM-exception"
|
||||||
|
url = "https://llvm.org/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://github.com/llvm/llvm-project/releases/download/llvmorg-{version}/llvm-project-{version}.src.tar.xz",
|
||||||
|
sha256="4579051e3c255fb4bb795d54324f5a7f3ef79bd9181e44293d7ee9a7f62aad9a",
|
||||||
|
)
|
||||||
|
host_deps = ["binutils"]
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run(
|
||||||
|
"cmake",
|
||||||
|
"-S",
|
||||||
|
self.source_dir / "llvm",
|
||||||
|
"-B",
|
||||||
|
self.build_dir,
|
||||||
|
"-GNinja",
|
||||||
|
f"-DDEFAULT_SYSROOT={self.sysroot}",
|
||||||
|
f"-DCMAKE_INSTALL_PREFIX={self.prefix}",
|
||||||
|
"-UBUILD_SHARED_LIBS",
|
||||||
|
"-UENABLE_STATIC",
|
||||||
|
"-DCMAKE_BUILD_TYPE=Release",
|
||||||
|
"-DLLVM_LINK_LLVM_DYLIB=ON",
|
||||||
|
"-DLLVM_ENABLE_FFI=ON",
|
||||||
|
"-DLLVM_ENABLE_EH=ON",
|
||||||
|
"-DLLVM_ENABLE_RTTI=ON",
|
||||||
|
"-DLLVM_ENABLE_PROJECTS=clang;lld;clang-tools-extra",
|
||||||
|
f"-DLLVM_DEFAULT_TARGET_TRIPLE={self.triple}",
|
||||||
|
f"-DLLVM_HOST_TRIPLE={self.triple}",
|
||||||
|
"-Wno-dev",
|
||||||
|
env={
|
||||||
|
"CFLAGS": self.profile["host_cflags"],
|
||||||
|
"CXXFLAGS": self.profile["host_cxxflags"],
|
||||||
|
"LDFLAGS": self.profile["host_ldflags"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
self.run("cmake", "--build", self.build_dir, f"-j{self.jobs}")
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
self.run("cmake", "--install", self.build_dir, env={"DESTDIR": str(self.dest_dir)})
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
version = "2.5.1"
|
||||||
|
revision = 1
|
||||||
|
description = "Lightweight pkg-config implementation (cross host build)"
|
||||||
|
license = "ISC"
|
||||||
|
url = "http://pkgconf.org/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://distfiles.ariadne.space/pkgconf/pkgconf-{version}.tar.xz",
|
||||||
|
sha256="cd05c9589b9f86ecf044c10a2269822bc9eb001eced2582cfffd658b0a50c243",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc"]
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run(
|
||||||
|
self.source_dir / "configure",
|
||||||
|
f"--prefix={self.prefix}",
|
||||||
|
"--with-system-libdir=/usr/lib",
|
||||||
|
"--with-system-includedir=/usr/include",
|
||||||
|
env={
|
||||||
|
"CFLAGS": self.profile["host_cflags"],
|
||||||
|
"LDFLAGS": self.profile["host_ldflags"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
autotools_build(self)
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
autotools_install(self)
|
||||||
|
|
||||||
|
triple = self.triple
|
||||||
|
bindir = f"{self.dest_dir}{self.prefix}/bin"
|
||||||
|
persdir = f"{self.dest_dir}{self.prefix}/share/pkgconfig/personality.d"
|
||||||
|
personality = f"""Triplet: {triple}
|
||||||
|
SysrootDir: /sysroot
|
||||||
|
DefaultSearchPaths: /sysroot/usr/lib/pkgconfig:/sysroot/usr/share/pkgconfig
|
||||||
|
SystemIncludePaths: /sysroot/usr/include
|
||||||
|
SystemLibraryPaths: /sysroot/usr/lib
|
||||||
|
"""
|
||||||
|
self.run(
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
f"set -e; "
|
||||||
|
f"mkdir -p {persdir}; "
|
||||||
|
f"cat > {persdir}/{triple}.personality <<'__EOF__'\n{personality}__EOF__\n"
|
||||||
|
f"ln -sf pkgconf {bindir}/{triple}-pkgconf; "
|
||||||
|
f"ln -sf pkgconf {bindir}/{triple}-pkg-config",
|
||||||
|
)
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
# Commonly used helpers, auto-loaded into every recipe.
|
|
||||||
|
|
||||||
def _toolchain_env(ctx):
|
|
||||||
sysroot_flag = " --sysroot=" + ctx.sysroot
|
|
||||||
return {
|
|
||||||
"CFLAGS": OPTIONS.cflags + sysroot_flag,
|
|
||||||
"CXXFLAGS": OPTIONS.cxxflags + sysroot_flag,
|
|
||||||
"LDFLAGS": OPTIONS.ldflags + sysroot_flag,
|
|
||||||
}
|
|
||||||
|
|
||||||
# Autotools
|
|
||||||
|
|
||||||
def autotools_configure(ctx, extra_args = [], extra_env = {}):
|
|
||||||
args = [
|
|
||||||
ctx.source_dir + "/configure",
|
|
||||||
"--prefix=" + ctx.prefix,
|
|
||||||
"--sysconfdir=/etc",
|
|
||||||
"--localstatedir=/var",
|
|
||||||
"--bindir=" + ctx.prefix + "/bin",
|
|
||||||
"--sbindir=" + ctx.prefix + "/bin",
|
|
||||||
"--libdir=" + ctx.prefix + "/lib",
|
|
||||||
"--with-sysroot=" + ctx.sysroot,
|
|
||||||
"--disable-static",
|
|
||||||
"--enable-shared",
|
|
||||||
]
|
|
||||||
args.append("--host=" + OPTIONS.target_triple)
|
|
||||||
args.extend(extra_args)
|
|
||||||
|
|
||||||
envs = _toolchain_env(ctx)
|
|
||||||
envs.update(extra_env)
|
|
||||||
|
|
||||||
ctx.run(args, env = envs)
|
|
||||||
|
|
||||||
def autotools_build(ctx, extra_args = []):
|
|
||||||
args = ["make", "-j" + str(ctx.jobs)]
|
|
||||||
args.extend(extra_args)
|
|
||||||
ctx.run(args)
|
|
||||||
|
|
||||||
def autotools_check(ctx, extra_args = []):
|
|
||||||
args = ["make", "check", "-j" + str(ctx.jobs)]
|
|
||||||
args.extend(extra_args)
|
|
||||||
ctx.run(args)
|
|
||||||
|
|
||||||
def autotools_install(ctx, pkg, extra_args = []):
|
|
||||||
args = ["make", "install", "DESTDIR=" + pkg.destdir]
|
|
||||||
args.extend(extra_args)
|
|
||||||
ctx.run(args)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
# Meson
|
|
||||||
|
|
||||||
def meson_configure(ctx, extra_args = []):
|
|
||||||
args = [
|
|
||||||
"meson",
|
|
||||||
"setup",
|
|
||||||
ctx.build_dir,
|
|
||||||
ctx.source_dir,
|
|
||||||
"--prefix=" + ctx.prefix,
|
|
||||||
]
|
|
||||||
args.extend(extra_args)
|
|
||||||
ctx.run(args, env = _toolchain_env(ctx))
|
|
||||||
|
|
||||||
def meson_build(ctx):
|
|
||||||
ctx.run(["meson", "compile", "-C", ctx.build_dir, "-j" + str(ctx.jobs)])
|
|
||||||
|
|
||||||
def meson_install(ctx, pkg):
|
|
||||||
ctx.run(["meson", "install", "-C", ctx.build_dir, "--destdir", pkg.destdir])
|
|
||||||
|
|
||||||
def meson(configure_args = [], build_args = [], install_args = []):
|
|
||||||
def _configure(ctx):
|
|
||||||
meson_configure(ctx, extra_args = configure_args)
|
|
||||||
def _build(ctx):
|
|
||||||
meson_build(ctx, extra_args = build_args)
|
|
||||||
def _install(ctx, pkg):
|
|
||||||
meson_install(ctx, pkg, extra_args = install_args)
|
|
||||||
return _configure, _build, _install
|
|
||||||
|
|
||||||
# Make
|
|
||||||
|
|
||||||
def make(ctx, target = None, extra_args = []):
|
|
||||||
args = ["make", "-C", ctx.source_dir, "O=" + ctx.build_dir,
|
|
||||||
"-j" + str(ctx.jobs)]
|
|
||||||
args.extend(extra_args)
|
|
||||||
if target:
|
|
||||||
args.append(target)
|
|
||||||
ctx.run(args)
|
|
||||||
|
|
||||||
def make_install(ctx, pkg, extra_args = []):
|
|
||||||
args = ["make", "-C", ctx.build_dir, "DESTDIR=" + pkg.destdir, "install"]
|
|
||||||
args.extend(extra_args)
|
|
||||||
ctx.run(args)
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
def profile():
|
||||||
|
arch = "x86_64"
|
||||||
|
libc = "glibc"
|
||||||
|
triple = f"{arch}-orchid-linux-gnu"
|
||||||
|
|
||||||
|
host_cflags = "-O2 -pipe"
|
||||||
|
host_cxxflags = host_cflags
|
||||||
|
host_ldflags = "-Wl,-O1 -Wl,--sort-common -Wl,--as-needed"
|
||||||
|
|
||||||
|
target_flags = " -march=x86-64-v3 -mtune=generic -fstack-clash-protection -fstack-protector-strong -fcf-protection"
|
||||||
|
cflags = host_cflags + target_flags
|
||||||
|
cxxflags = cflags
|
||||||
|
ldflags = host_ldflags + " -Wl,-z,now -Wl,-z,pack-relative-relocs"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"arch": arch,
|
||||||
|
"libc": libc,
|
||||||
|
"triple": triple,
|
||||||
|
"container_image": "localhost/orchid-builder:latest",
|
||||||
|
"host_cflags": host_cflags,
|
||||||
|
"host_cxxflags": host_cxxflags,
|
||||||
|
"host_ldflags": host_ldflags,
|
||||||
|
"cflags": cflags,
|
||||||
|
"cxxflags": cxxflags,
|
||||||
|
"ldflags": ldflags,
|
||||||
|
"prefix": "/usr",
|
||||||
|
"bindir": "/usr/bin",
|
||||||
|
"sbindir": "/usr/bin",
|
||||||
|
"libdir": "/usr/lib",
|
||||||
|
"libexecdir": "/usr/libexec",
|
||||||
|
"includedir": "/usr/include",
|
||||||
|
"sysconfdir": "/etc",
|
||||||
|
"localstatedir": "/var",
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
def profile():
|
||||||
|
arch = "x86_64"
|
||||||
|
libc = "musl"
|
||||||
|
triple = f"{arch}-orchid-linux-{libc}"
|
||||||
|
|
||||||
|
host_cflags = "-O2 -pipe"
|
||||||
|
host_cxxflags = host_cflags
|
||||||
|
host_ldflags = "-Wl,-O1 -Wl,--sort-common -Wl,--as-needed"
|
||||||
|
|
||||||
|
target_flags = " -march=x86-64-v3 -mtune=generic -fstack-clash-protection -fstack-protector-strong -fcf-protection"
|
||||||
|
cflags = host_cflags + target_flags
|
||||||
|
cxxflags = cflags
|
||||||
|
ldflags = host_ldflags + " -Wl,-z,now -Wl,-z,pack-relative-relocs"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"arch": arch,
|
||||||
|
"libc": libc,
|
||||||
|
"triple": triple,
|
||||||
|
"container_image": "localhost/orchid-builder:latest",
|
||||||
|
"host_cflags": host_cflags,
|
||||||
|
"host_cxxflags": host_cxxflags,
|
||||||
|
"host_ldflags": host_ldflags,
|
||||||
|
"cflags": cflags,
|
||||||
|
"cxxflags": cxxflags,
|
||||||
|
"ldflags": ldflags,
|
||||||
|
"prefix": "/usr",
|
||||||
|
"bindir": "/usr/bin",
|
||||||
|
"sbindir": "/usr/bin",
|
||||||
|
"libdir": "/usr/lib",
|
||||||
|
"libexecdir": "/usr/libexec",
|
||||||
|
"includedir": "/usr/include",
|
||||||
|
"sysconfdir": "/etc",
|
||||||
|
"localstatedir": "/var",
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"ignore": [
|
||||||
|
"recipes",
|
||||||
|
"host-recipes"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
version = "5.3"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU Bourne-Again SHell"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
url = "https://www.gnu.org/software/bash/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/bash/bash-{version}.tar.gz",
|
||||||
|
sha256="0d5cd86965f869a26cf64f4b71be7b96f90a3ba8b3d74e27e8e9d9d5550f31ba",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc"]
|
||||||
|
deps = [profile["libc"], "ncurses", "readline"]
|
||||||
|
|
||||||
|
configure, build, install = autotools(
|
||||||
|
configure_args=[
|
||||||
|
"--without-bash-malloc",
|
||||||
|
"--disable-nls",
|
||||||
|
"--with-curses",
|
||||||
|
"--enable-readline",
|
||||||
|
"--with-installed-readline",
|
||||||
|
],
|
||||||
|
configure_env={
|
||||||
|
"bash_cv_termcap_lib": "libtinfo",
|
||||||
|
"CFLAGS": profile["cflags"] + " -std=gnu17",
|
||||||
|
"CFLAGS_FOR_BUILD": profile["host_cflags"] + " -std=gnu17",
|
||||||
|
},
|
||||||
|
# HACK: Fixes cross-compile issues
|
||||||
|
build_args=[
|
||||||
|
"READLINE_LDFLAGS=-L=/usr/lib",
|
||||||
|
"HISTORY_LDFLAGS=-L=/usr/lib",
|
||||||
|
],
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
version = "9.6"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU core utilities (file, shell, and text manipulation)"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
url = "https://www.gnu.org/software/coreutils/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/coreutils/coreutils-{version}.tar.xz",
|
||||||
|
sha256="7a0124327b398fd9eb1a6abde583389821422c744ffa10734b24f557610d3283",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc"]
|
||||||
|
deps = [profile["libc"]]
|
||||||
|
|
||||||
|
configure, build, install = autotools(
|
||||||
|
configure_args=[
|
||||||
|
"--enable-no-install-program=kill,uptime",
|
||||||
|
"--without-selinux",
|
||||||
|
"--without-openssl",
|
||||||
|
],
|
||||||
|
configure_env={"FORCE_UNSAFE_CONFIGURE": "1"},
|
||||||
|
)
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
version = "2.41"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU C library"
|
||||||
|
license = "LGPL-2.1-or-later"
|
||||||
|
url = "https://www.gnu.org/software/libc/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/glibc/glibc-{version}.tar.xz",
|
||||||
|
sha256="a5a26b22f545d6b7d7b3dd828e11e428f24f4fac43c934fb071b6a7d0828e901",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc-bootstrap"]
|
||||||
|
deps = ["linux-headers"]
|
||||||
|
|
||||||
|
build_if = profile["libc"] == "glibc"
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
autotools_configure(
|
||||||
|
self,
|
||||||
|
[
|
||||||
|
f"--build={self.triple}",
|
||||||
|
f"--with-headers={self.sysroot}{self.profile['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()
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
version = "16.1.0"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU C++ standard library"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
url = "https://gcc.gnu.org/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
|
||||||
|
sha256="50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
|
||||||
|
)
|
||||||
|
host_deps = ["binutils", "gcc"]
|
||||||
|
deps = [profile["libc"]]
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run(
|
||||||
|
self.source_dir / "configure",
|
||||||
|
f"--target={self.triple}",
|
||||||
|
f"--prefix={self.profile['prefix']}",
|
||||||
|
f"--libdir={self.profile['libdir']}",
|
||||||
|
f"--with-sysroot={self.sysroot}",
|
||||||
|
f"--with-build-sysroot={self.sysroot}",
|
||||||
|
f"--with-gxx-include-dir={self.profile['includedir']}/c++/{version}",
|
||||||
|
f"--with-toolexeclibdir={self.profile['libdir']}",
|
||||||
|
"--enable-languages=c,c++",
|
||||||
|
"--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": self.profile["host_cflags"],
|
||||||
|
"CXXFLAGS": self.profile["host_cxxflags"],
|
||||||
|
"LDFLAGS": self.profile["host_ldflags"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
self.run("make", f"-j{self.jobs}", "all-target-libstdc++-v3")
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
self.run(
|
||||||
|
"make",
|
||||||
|
"install-target-libstdc++-v3",
|
||||||
|
env={"DESTDIR": str(self.dest_dir)},
|
||||||
|
)
|
||||||
|
self.run("find", self.dest_dir, "-name", "*.la", "-delete")
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
version = "12.2.0"
|
||||||
|
revision = 1
|
||||||
|
description = "Modern, secure, portable, multiprotocol bootloader and boot manager"
|
||||||
|
license = "BSD-2-Clause"
|
||||||
|
url = "https://limine-bootloader.org"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://github.com/Limine-Bootloader/Limine/releases/download/v{version}/limine-{version}.tar.gz",
|
||||||
|
sha256="db8a119878cfeead63c0a78236c577c40539c5759496950ea0ed32a6cf567865",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc"]
|
||||||
|
deps = [profile["libc"]]
|
||||||
|
|
||||||
|
build_if = profile["arch"] in ("x86_64", "aarch64", "riscv64", "loongarch64")
|
||||||
|
|
||||||
|
_arch_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_args.get(profile["arch"], [])],
|
||||||
|
configure_env={"TOOLCHAIN_FOR_TARGET": profile["triple"] + "-"},
|
||||||
|
)
|
||||||
|
|
||||||
|
subpackages = [
|
||||||
|
subpackage(
|
||||||
|
"limine-uefi",
|
||||||
|
description="UEFI files",
|
||||||
|
files=[
|
||||||
|
"usr/share/limine/BOOT*.EFI",
|
||||||
|
"usr/share/limine/limine-uefi-*.bin",
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
if profile["arch"] == "x86_64":
|
||||||
|
subpackages.append(
|
||||||
|
subpackage(
|
||||||
|
"limine-bios",
|
||||||
|
description="BIOS files",
|
||||||
|
files=["usr/share/limine/limine-bios*"],
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
name = "limine"
|
|
||||||
version = "12.2.0"
|
|
||||||
revision = 1
|
|
||||||
description = "Modern, secure, portable, multiprotocol bootloader and boot manager"
|
|
||||||
license = "BSD-2-Clause"
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
def configure(ctx):
|
|
||||||
toolchain = OPTIONS.target_triple + "-"
|
|
||||||
autotools_configure(ctx, extra_env = {
|
|
||||||
"TOOLCHAIN_FOR_TARGET": toolchain,
|
|
||||||
"LD_FOR_TARGET": toolchain + "ld",
|
|
||||||
"OBJCOPY_FOR_TARGET": toolchain + "objcopy",
|
|
||||||
"OBJDUMP_FOR_TARGET": toolchain + "objdump",
|
|
||||||
})
|
|
||||||
|
|
||||||
_, build, install = autotools()
|
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
version = "7.0.9"
|
||||||
|
revision = 2
|
||||||
|
description = "Linux kernel headers for userspace development"
|
||||||
|
license = "GPL-2.0-only"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{version}.tar.xz",
|
||||||
|
sha256="ac07acdf76cf4621cc5187a2670270a1a699533c8a6b225e4878c416ad83f1c4",
|
||||||
|
)
|
||||||
|
|
||||||
|
linux_archs = {"aarch64": "arm64"}
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
# Stage the source into the (writable) build dir before invoking the kernel
|
||||||
|
# build system, which is not happy with a read-only source tree.
|
||||||
|
self.run("cp", "-rp", f"{self.source_dir}/.", self.build_dir)
|
||||||
|
arch = linux_archs.get(self.profile["arch"], self.profile["arch"])
|
||||||
|
self.run("make", "headers_install", f"ARCH={arch}")
|
||||||
|
self.run(
|
||||||
|
"find",
|
||||||
|
self.build_dir / "usr/include",
|
||||||
|
"-type",
|
||||||
|
"f",
|
||||||
|
"!",
|
||||||
|
"-name",
|
||||||
|
"*.h",
|
||||||
|
"-delete",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
self.run("mkdir", "-p", self.dest_dir / self.profile["prefix"].lstrip("/"))
|
||||||
|
self.run(
|
||||||
|
"cp",
|
||||||
|
"-rp",
|
||||||
|
self.build_dir / "usr/include",
|
||||||
|
str(self.dest_dir) + self.profile["prefix"],
|
||||||
|
)
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,48 @@
|
|||||||
|
version = "7.0.9"
|
||||||
|
revision = 2
|
||||||
|
description = "Linux kernel"
|
||||||
|
license = "GPL-2.0-only"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{version}.tar.xz",
|
||||||
|
sha256="ac07acdf76cf4621cc5187a2670270a1a699533c8a6b225e4878c416ad83f1c4",
|
||||||
|
)
|
||||||
|
host_deps = ["binutils", "gcc"]
|
||||||
|
deps = [profile["libc"]]
|
||||||
|
|
||||||
|
linux_archs = {"aarch64": "arm64"}
|
||||||
|
linux_subarchs = {"x86_64": "x86"}
|
||||||
|
|
||||||
|
|
||||||
|
def _make(self, *extra):
|
||||||
|
arch = linux_archs.get(self.profile["arch"], self.profile["arch"])
|
||||||
|
subarch = linux_subarchs.get(self.profile["arch"])
|
||||||
|
subarch_arg = (f"SUBARCH={subarch}",) if subarch else ()
|
||||||
|
return (
|
||||||
|
"make",
|
||||||
|
f"ARCH={arch}",
|
||||||
|
*subarch_arg,
|
||||||
|
f"CROSS_COMPILE={self.triple}-",
|
||||||
|
f"-j{self.jobs}",
|
||||||
|
*extra,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run("cp", "-rp", f"{self.source_dir}/.", self.build_dir)
|
||||||
|
self.run(
|
||||||
|
"cp", self.files / f"config.{self.profile['arch']}", self.build_dir / ".config"
|
||||||
|
)
|
||||||
|
self.run(*_make(self, "olddefconfig"))
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
self.run(*_make(self))
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
self.run(
|
||||||
|
"install",
|
||||||
|
"-Dm644",
|
||||||
|
self.build_dir / "arch/x86/boot/bzImage",
|
||||||
|
self.dest_dir / f"boot/vmlinuz-{self.version}",
|
||||||
|
)
|
||||||
@@ -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,
|
|
||||||
)
|
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
version = "4.4.1"
|
||||||
|
revision = 1
|
||||||
|
description = "GNU make build automation tool"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
url = "https://www.gnu.org/software/make/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/make/make-{version}.tar.gz",
|
||||||
|
sha256="dd16fb1d67bfab79a72f5e8390735c49e3e8e70b4945a15ab1f81ddb78658fb3",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc"]
|
||||||
|
deps = [profile["libc"]]
|
||||||
|
|
||||||
|
configure, build, install = autotools(configure_args=["--without-guile"])
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
version = "1.2.6"
|
||||||
|
revision = 1
|
||||||
|
description = "Small, standards-conformant implementation of libc"
|
||||||
|
license = "MIT"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://musl.libc.org/releases/musl-{version}.tar.gz",
|
||||||
|
sha256="d585fd3b613c66151fc3249e8ed44f77020cb5e6c1e635a616d3f9f82460512a",
|
||||||
|
)
|
||||||
|
host_deps = ["binutils", "gcc-bootstrap"]
|
||||||
|
|
||||||
|
build_if = profile["libc"] == "musl"
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
self.run(
|
||||||
|
self.source_dir / "configure",
|
||||||
|
f"--target={self.triple}",
|
||||||
|
f"--prefix={self.profile['prefix']}",
|
||||||
|
"--syslibdir=/lib",
|
||||||
|
env={
|
||||||
|
"CC": f"{self.triple}-gcc",
|
||||||
|
"CFLAGS": self.profile["cflags"],
|
||||||
|
"LDFLAGS": self.profile["ldflags"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_, build, install = autotools()
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
name = "musl"
|
|
||||||
version = "1.2.6"
|
|
||||||
revision = 1
|
|
||||||
description = "Musl C library"
|
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
source = {
|
|
||||||
"url": f"https://musl.libc.org/releases/musl-{version}.tar.gz",
|
|
||||||
"sha256": "d585fd3b613c66151fc3249e8ed44f77020cb5e6c1e635a616d3f9f82460512a",
|
|
||||||
"strip_components": 1,
|
|
||||||
}
|
|
||||||
|
|
||||||
host_deps = ["binutils", "gcc"]
|
|
||||||
|
|
||||||
def configure(ctx):
|
|
||||||
ctx.run(
|
|
||||||
[
|
|
||||||
ctx.source_dir + "/configure",
|
|
||||||
"--prefix=/usr",
|
|
||||||
"--syslibdir=/lib",
|
|
||||||
"--target=" + OPTIONS.target_triple,
|
|
||||||
],
|
|
||||||
env = {
|
|
||||||
"CC": OPTIONS.target_triple + "-gcc",
|
|
||||||
"CFLAGS": OPTIONS.cflags,
|
|
||||||
"LDFLAGS": OPTIONS.ldflags,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
build = autotools_build
|
|
||||||
install = autotools_install
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
version = "6.5"
|
||||||
|
revision = 2
|
||||||
|
description = "Terminal control library with wide-character support"
|
||||||
|
license = "MIT"
|
||||||
|
url = "https://invisible-island.net/ncurses/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://invisible-mirror.net/archives/ncurses/ncurses-{version}.tar.gz",
|
||||||
|
sha256="136d91bc269a9a5785e5f9e980bc76ab57428f604ce3e5a5a90cebc767971cc6",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc"]
|
||||||
|
deps = [profile["libc"], "libstdc++"]
|
||||||
|
|
||||||
|
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",
|
||||||
|
"--with-tic-path=/usr/bin/tic",
|
||||||
|
"--with-infocmp-path=/usr/bin/infocmp",
|
||||||
|
"--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(self):
|
||||||
|
autotools_install(self, [f"DESTDIR={self.dest_dir}"])
|
||||||
|
libdir = self.dest_dir / "usr/lib"
|
||||||
|
pcdir = libdir / "pkgconfig"
|
||||||
|
for name in ("ncurses", "curses"):
|
||||||
|
self.run("ln", "-sf", "libncursesw.so", libdir / f"lib{name}.so")
|
||||||
|
self.run("ln", "-sf", "libncursesw.a", libdir / f"lib{name}.a")
|
||||||
|
self.run("ln", "-sf", "libtinfow.so", libdir / "libtinfo.so")
|
||||||
|
self.run("ln", "-sf", "libtinfow.a", libdir / "libtinfo.a")
|
||||||
|
self.run("ln", "-sf", "ncursesw.pc", pcdir / "ncurses.pc")
|
||||||
|
self.run("ln", "-sf", "tinfow.pc", pcdir / "tinfo.pc")
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
version = "3.4.1"
|
||||||
|
revision = 1
|
||||||
|
description = "Cryptography and TLS library (OpenSSL)"
|
||||||
|
license = "Apache-2.0"
|
||||||
|
url = "https://www.openssl.org/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://github.com/openssl/openssl/releases/download/openssl-{version}/openssl-{version}.tar.gz",
|
||||||
|
sha256="?",
|
||||||
|
)
|
||||||
|
host_deps = ["binutils", "gcc"]
|
||||||
|
deps = ["zlib"]
|
||||||
|
|
||||||
|
ossl_targets = {
|
||||||
|
"x86_64": "linux-x86_64",
|
||||||
|
"aarch64": "linux-aarch64",
|
||||||
|
"riscv64": "linux64-riscv64",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
target = ossl_targets.get(self.profile["arch"])
|
||||||
|
if target is None:
|
||||||
|
raise ValueError(f"openssl: unsupported arch {self.profile['arch']}")
|
||||||
|
|
||||||
|
self.run(
|
||||||
|
self.source_dir / "Configure",
|
||||||
|
target,
|
||||||
|
f"--prefix={self.profile['prefix']}",
|
||||||
|
"--openssldir=/etc/ssl",
|
||||||
|
"--libdir=lib",
|
||||||
|
"shared",
|
||||||
|
"zlib",
|
||||||
|
"no-tests",
|
||||||
|
"no-static-engine",
|
||||||
|
"enable-ktls",
|
||||||
|
env={
|
||||||
|
"CC": f"{self.triple}-gcc",
|
||||||
|
"AR": f"{self.triple}-ar",
|
||||||
|
"RANLIB": f"{self.triple}-ranlib",
|
||||||
|
"CFLAGS": self.profile["cflags"],
|
||||||
|
"LDFLAGS": self.profile["ldflags"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
self.run("make", f"-j{self.jobs}")
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
self.run(
|
||||||
|
"make", "install_sw", "install_ssldirs", env={"DESTDIR": str(self.dest_dir)}
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
version = "3.3.3"
|
||||||
|
revision = 1
|
||||||
|
description = "Lightweight pkg-config implementation"
|
||||||
|
license = "ISC"
|
||||||
|
url = "http://pkgconf.org/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://distfiles.ariadne.space/pkgconf/pkgconf-{version}.tar.xz",
|
||||||
|
sha256="?",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc"]
|
||||||
|
deps = [profile["libc"]]
|
||||||
|
|
||||||
|
configure, build, install = autotools(
|
||||||
|
configure_args=[
|
||||||
|
f"--with-system-libdir={profile['libdir']}",
|
||||||
|
f"--with-system-includedir={profile['includedir']}",
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
version = "8.3"
|
||||||
|
revision = 1
|
||||||
|
description = "Library for command-line editing"
|
||||||
|
license = "GPL-3.0-or-later"
|
||||||
|
url = "https://tiswww.case.edu/php/chet/readline/rltop.html"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://ftp.gnu.org/gnu/readline/readline-{version}.tar.gz",
|
||||||
|
sha256="fe5383204467828cd495ee8d1d3c037a7eba1389c22bc6a041f627976f9061cc",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc"]
|
||||||
|
deps = [profile["libc"], "ncurses"]
|
||||||
|
|
||||||
|
configure, build, _ = autotools(
|
||||||
|
configure_args=["--with-curses", "--with-shared-termcap-library"],
|
||||||
|
# Force linking against system terminfo, not readline's internal termcap stub.
|
||||||
|
configure_env={"bash_cv_termcap_lib": "libtinfo"},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
# readline overwrites DESTDIR on its own; pass explicitly.
|
||||||
|
autotools_install(self, [f"DESTDIR={self.dest_dir}"])
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
version = "5.6.3"
|
||||||
|
revision = 1
|
||||||
|
description = "LZMA/XZ compression tools and library"
|
||||||
|
license = "0BSD AND GPL-2.0-or-later AND LGPL-2.1-or-later"
|
||||||
|
url = "https://tukaani.org/xz/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://github.com/tukaani-project/xz/releases/download/v{version}/xz-{version}.tar.xz",
|
||||||
|
sha256="db0590629b6f0fa36e74aea5f9731dc6f8df068ce7b7bafa45301832a5eebc3a",
|
||||||
|
)
|
||||||
|
host_deps = ["autoconf", "automake", "binutils", "gcc"]
|
||||||
|
deps = [profile["libc"]]
|
||||||
|
|
||||||
|
configure, build, install = autotools(configure_args=["--disable-doc"])
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
version = "1.3.2"
|
||||||
|
revision = 1
|
||||||
|
description = "Lossless data-compression library"
|
||||||
|
license = "Zlib"
|
||||||
|
url = "https://zlib.net/"
|
||||||
|
source = tarball(
|
||||||
|
url=f"https://github.com/madler/zlib/releases/download/v{version}/zlib-{version}.tar.gz",
|
||||||
|
sha256="?",
|
||||||
|
)
|
||||||
|
host_deps = ["binutils", "gcc"]
|
||||||
|
deps = [profile["libc"]]
|
||||||
|
|
||||||
|
|
||||||
|
def configure(self):
|
||||||
|
# zlib ships its own ./configure; doesn't grok autoconf flags.
|
||||||
|
self.run(
|
||||||
|
self.source_dir / "configure",
|
||||||
|
f"--prefix={self.profile['prefix']}",
|
||||||
|
f"--libdir={self.profile['libdir']}",
|
||||||
|
f"--sharedlibdir={self.profile['libdir']}",
|
||||||
|
env={
|
||||||
|
"CC": f"{self.triple}-gcc",
|
||||||
|
"AR": f"{self.triple}-ar",
|
||||||
|
"RANLIB": f"{self.triple}-ranlib",
|
||||||
|
"CFLAGS": self.profile["cflags"],
|
||||||
|
"LDFLAGS": self.profile["ldflags"],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def build(self):
|
||||||
|
self.run("make", f"-j{self.jobs}")
|
||||||
|
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
self.run("make", "install", env={"DESTDIR": str(self.dest_dir)})
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
"""Orchid build system"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
Executable
+12
@@ -0,0 +1,12 @@
|
|||||||
|
#!/usr/bin/python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
_HERE = os.path.dirname(os.path.realpath(__file__))
|
||||||
|
_ROOT = os.path.dirname(_HERE)
|
||||||
|
if _ROOT not in sys.path:
|
||||||
|
sys.path.insert(0, _ROOT)
|
||||||
|
|
||||||
|
from src.cli import main
|
||||||
|
|
||||||
|
raise SystemExit(main())
|
||||||
+135
@@ -0,0 +1,135 @@
|
|||||||
|
from src.container import Container
|
||||||
|
from src.recipe import Recipe
|
||||||
|
from src.source import Subpackage
|
||||||
|
|
||||||
|
|
||||||
|
def _info_args(
|
||||||
|
name: str,
|
||||||
|
version: str,
|
||||||
|
revision: int,
|
||||||
|
arch: str,
|
||||||
|
origin: str,
|
||||||
|
description: str,
|
||||||
|
license: str,
|
||||||
|
url: str,
|
||||||
|
maintainer: str,
|
||||||
|
depends: list[str],
|
||||||
|
) -> list[str]:
|
||||||
|
args = [
|
||||||
|
"--info",
|
||||||
|
f"name:{name}",
|
||||||
|
"--info",
|
||||||
|
f"version:{version}-r{revision}",
|
||||||
|
"--info",
|
||||||
|
f"arch:{arch}",
|
||||||
|
"--info",
|
||||||
|
f"origin:{origin}",
|
||||||
|
]
|
||||||
|
if description:
|
||||||
|
args += ["--info", f"description:{description}"]
|
||||||
|
if license:
|
||||||
|
args += ["--info", f"license:{license}"]
|
||||||
|
if url:
|
||||||
|
args += ["--info", f"url:{url}"]
|
||||||
|
if maintainer:
|
||||||
|
args += ["--info", f"packager:{maintainer}"]
|
||||||
|
for d in depends:
|
||||||
|
args += ["--info", f"depends:{d}"]
|
||||||
|
return args
|
||||||
|
|
||||||
|
|
||||||
|
def mkpkg_base(container: Container, recipe: Recipe, arch: str) -> None:
|
||||||
|
name = recipe.name
|
||||||
|
deps = list(recipe.deps) + list(recipe.run_deps)
|
||||||
|
args = [
|
||||||
|
"apk",
|
||||||
|
"mkpkg",
|
||||||
|
"--files",
|
||||||
|
f"/dest/{name}",
|
||||||
|
"--output",
|
||||||
|
f"/pkgs/{name}-{recipe.version}-r{recipe.revision}.apk",
|
||||||
|
*_info_args(
|
||||||
|
name,
|
||||||
|
recipe.version,
|
||||||
|
recipe.revision,
|
||||||
|
arch,
|
||||||
|
recipe.name,
|
||||||
|
recipe.description,
|
||||||
|
recipe.license,
|
||||||
|
recipe.url,
|
||||||
|
recipe.maintainer,
|
||||||
|
deps,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
container.exec(args)
|
||||||
|
|
||||||
|
|
||||||
|
def mkpkg_subpackage(
|
||||||
|
container: Container, recipe: Recipe, sub: Subpackage, arch: str
|
||||||
|
) -> None:
|
||||||
|
args = [
|
||||||
|
"apk",
|
||||||
|
"mkpkg",
|
||||||
|
"--files",
|
||||||
|
f"/dest/{sub.name}",
|
||||||
|
"--output",
|
||||||
|
f"/pkgs/{sub.name}-{recipe.version}-r{recipe.revision}.apk",
|
||||||
|
*_info_args(
|
||||||
|
sub.name,
|
||||||
|
recipe.version,
|
||||||
|
recipe.revision,
|
||||||
|
arch,
|
||||||
|
recipe.name,
|
||||||
|
sub.description,
|
||||||
|
sub.license,
|
||||||
|
sub.url,
|
||||||
|
sub.maintainer,
|
||||||
|
[recipe.name],
|
||||||
|
),
|
||||||
|
]
|
||||||
|
container.exec(args)
|
||||||
|
|
||||||
|
|
||||||
|
def index(container: Container) -> None:
|
||||||
|
container.exec_shell(
|
||||||
|
"apk mkndx --allow-untrusted -o /pkgs/Packages.adb /pkgs/*.apk"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def sysroot_install(container: Container, pkgs: list[str], *, initdb: bool) -> None:
|
||||||
|
if not pkgs:
|
||||||
|
return
|
||||||
|
args = [
|
||||||
|
"apk",
|
||||||
|
"add",
|
||||||
|
"--root",
|
||||||
|
"/sysroot",
|
||||||
|
"--allow-untrusted",
|
||||||
|
"--no-network",
|
||||||
|
"--repository",
|
||||||
|
"/pkgs/Packages.adb",
|
||||||
|
]
|
||||||
|
if initdb:
|
||||||
|
args.append("--initdb")
|
||||||
|
args += pkgs
|
||||||
|
container.exec(args)
|
||||||
|
|
||||||
|
|
||||||
|
def sysroot_initialized(container: Container) -> bool:
|
||||||
|
rc = container.exec(["test", "-f", "/sysroot/lib/apk/db/installed"], check=False)
|
||||||
|
return rc == 0
|
||||||
|
|
||||||
|
|
||||||
|
def split_subpackage_script(base: str, dest: str, pattern: str) -> str:
|
||||||
|
# Move matching files from /dest/<base> to /dest/<dest>, preserving dirs.
|
||||||
|
# TODO: Make this cleaner.
|
||||||
|
return (
|
||||||
|
f"set -e; "
|
||||||
|
f"cd /dest/{base}; "
|
||||||
|
f"find . -depth -path './{pattern}' -print 2>/dev/null | while IFS= read -r f; do "
|
||||||
|
f' rel="${{f#./}}"; '
|
||||||
|
f' target=/dest/{dest}/"$rel"; '
|
||||||
|
f' mkdir -p "$(dirname "$target")"; '
|
||||||
|
f' mv "$f" "$target"; '
|
||||||
|
f"done"
|
||||||
|
)
|
||||||
-46
@@ -1,46 +0,0 @@
|
|||||||
use crate::config::Config;
|
|
||||||
use crate::recipe::OutputPackage;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ApkPackagePlan {
|
|
||||||
pub args: Vec<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn mkpkg_plan(
|
|
||||||
config: &Config,
|
|
||||||
package: &OutputPackage,
|
|
||||||
files_root: &Path,
|
|
||||||
output_dir: &Path,
|
|
||||||
) -> ApkPackagePlan {
|
|
||||||
let file_name = format!(
|
|
||||||
"{}-{}-r{}.apk",
|
|
||||||
package.name, package.version, package.revision
|
|
||||||
);
|
|
||||||
let output = output_dir.join(file_name);
|
|
||||||
let version = format!("{}-r{}", package.version, package.revision);
|
|
||||||
let mut args = vec![
|
|
||||||
"mkpkg".to_owned(),
|
|
||||||
"--files".to_owned(),
|
|
||||||
files_root.display().to_string(),
|
|
||||||
"--output".to_owned(),
|
|
||||||
output.display().to_string(),
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("name:{}", package.name),
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("version:{version}"),
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("arch:{}", config.target_arch),
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("description:{}", package.description),
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("license:{}", package.license),
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("origin:{}", package.recipe),
|
|
||||||
];
|
|
||||||
for dep in &package.deps {
|
|
||||||
args.push("--info".to_owned());
|
|
||||||
args.push(format!("depends:{dep}"));
|
|
||||||
}
|
|
||||||
ApkPackagePlan { args }
|
|
||||||
}
|
|
||||||
-949
@@ -1,949 +0,0 @@
|
|||||||
use crate::apk;
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::graph::PackageGraph;
|
|
||||||
use crate::log;
|
|
||||||
use crate::patches;
|
|
||||||
use crate::phase::{PhaseCommand, PhaseEnv, SourceDir, collect_phase_commands};
|
|
||||||
use crate::recipe::{OutputPackage, PackageKind, Recipe, RecipeSet};
|
|
||||||
use crate::source;
|
|
||||||
use anyhow::{Context, Result, anyhow, bail};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::cell::{Cell, RefCell};
|
|
||||||
use std::collections::HashSet;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::process::{Command, Stdio};
|
|
||||||
|
|
||||||
const C_SOURCE: &str = "/source";
|
|
||||||
const C_BUILD: &str = "/builddir";
|
|
||||||
const C_DEST: &str = "/dest";
|
|
||||||
const C_SYSROOT: &str = "/sysroot";
|
|
||||||
const HOST_PREFIX: &str = "/usr/local";
|
|
||||||
const TARGET_PREFIX: &str = "/usr";
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Builder {
|
|
||||||
repo: PathBuf,
|
|
||||||
config: Config,
|
|
||||||
skip_checks: bool,
|
|
||||||
preflight_done: Cell<bool>,
|
|
||||||
built_recipes: RefCell<HashSet<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Builder {
|
|
||||||
pub fn new(repo: PathBuf, config: Config, skip_checks: bool) -> Self {
|
|
||||||
Self {
|
|
||||||
repo,
|
|
||||||
config,
|
|
||||||
skip_checks,
|
|
||||||
preflight_done: Cell::new(false),
|
|
||||||
built_recipes: RefCell::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(
|
|
||||||
&self,
|
|
||||||
recipes: &RecipeSet,
|
|
||||||
graph: &PackageGraph,
|
|
||||||
package: Option<&str>,
|
|
||||||
rebuild: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
self.preflight_container()?;
|
|
||||||
let (label, order) = match package {
|
|
||||||
Some(p) => (p.to_owned(), graph.build_order(p)?),
|
|
||||||
None => ("<all>".to_owned(), graph.build_order_all()?),
|
|
||||||
};
|
|
||||||
log::step(
|
|
||||||
"plan",
|
|
||||||
&format!("{label}: {} package(s) [{}]", order.len(), order.join(", ")),
|
|
||||||
);
|
|
||||||
for output_name in order {
|
|
||||||
let output = graph.output(&output_name)?;
|
|
||||||
let recipe = recipes.recipe_for_package(&output_name)?;
|
|
||||||
self.ensure_source(recipe)?;
|
|
||||||
// Only force a rebuild of the explicitly requested package;
|
|
||||||
// dependencies stay cached when their manifest is up to date.
|
|
||||||
// With no package (build all), every package honours --rebuild.
|
|
||||||
let force = rebuild && package.map_or(true, |p| p == output_name);
|
|
||||||
match output.kind {
|
|
||||||
PackageKind::Host => self.build_host(recipe, output, force)?,
|
|
||||||
PackageKind::Target => self.build_target(recipe, output, force)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.repo_index()?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch(&self, recipes: &RecipeSet, package: &str) -> Result<()> {
|
|
||||||
let recipe = recipes.recipe_by_user_ref(package)?;
|
|
||||||
log::step("fetch", &recipe.key());
|
|
||||||
source::fetch_sources(recipe, &self.source_cache_dir())?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn shell(&self, recipes: &RecipeSet, graph: &PackageGraph, package: &str) -> Result<()> {
|
|
||||||
graph.output(package)?;
|
|
||||||
let recipe = recipes.recipe_for_package(package)?;
|
|
||||||
self.preflight_container()?;
|
|
||||||
let source = self.unpacked_source_dir(recipe);
|
|
||||||
let build = self.build_dir(recipe);
|
|
||||||
fs::create_dir_all(&source)?;
|
|
||||||
fs::create_dir_all(&build)?;
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("run")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("-it")
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:{C_SOURCE}:ro", source.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:{C_BUILD}", build.display()))
|
|
||||||
.arg("-w")
|
|
||||||
.arg(C_BUILD)
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg("/bin/sh")
|
|
||||||
.status()?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("container shell exited with {status}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn repo_index(&self) -> Result<()> {
|
|
||||||
self.preflight_container()?;
|
|
||||||
let repo = self.pkgs_dir();
|
|
||||||
fs::create_dir_all(&repo)?;
|
|
||||||
// Nothing to index yet — leave it alone so callers don't trip on a
|
|
||||||
// failing `*.apk` glob.
|
|
||||||
let has_apks = fs::read_dir(&repo)?.any(|e| {
|
|
||||||
e.ok()
|
|
||||||
.and_then(|e| e.path().extension().map(|x| x == "apk"))
|
|
||||||
.unwrap_or(false)
|
|
||||||
});
|
|
||||||
if !has_apks {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
log::step(
|
|
||||||
"index",
|
|
||||||
&format!("signing repository at {}", repo.display()),
|
|
||||||
);
|
|
||||||
let key = self.abs_config_path(&self.config.signing_key);
|
|
||||||
let pubkey = self.abs_config_path(&self.config.signing_pubkey);
|
|
||||||
if !key.exists() || !pubkey.exists() {
|
|
||||||
bail!("signing key is not configured or missing; run `distro init-key` first");
|
|
||||||
}
|
|
||||||
let index_name = "APKINDEX.tar.gz";
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("run")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/repo", repo.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/keys/distro.rsa:ro", key.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!(
|
|
||||||
"{}:/etc/apk/keys/distro.rsa.pub:ro",
|
|
||||||
pubkey.display()
|
|
||||||
))
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg("/bin/sh")
|
|
||||||
.arg("-lc")
|
|
||||||
.arg(format!(
|
|
||||||
"cd /repo && apk --sign-key /keys/distro.rsa mkndx -o {index_name} *.apk"
|
|
||||||
))
|
|
||||||
.status()
|
|
||||||
.context("failed to run repository index command")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("repository index command failed with {status}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn init_key(&self) -> Result<()> {
|
|
||||||
let key = self.abs_config_path(&self.config.signing_key);
|
|
||||||
let pubkey = self.abs_config_path(&self.config.signing_pubkey);
|
|
||||||
if key.exists() && pubkey.exists() {
|
|
||||||
log::skip("init-key", "signing key already present");
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
log::step("init-key", &format!("generating {}", key.display()));
|
|
||||||
let key_dir = key
|
|
||||||
.parent()
|
|
||||||
.ok_or_else(|| anyhow!("invalid signing key path"))?;
|
|
||||||
let key_name = key
|
|
||||||
.file_name()
|
|
||||||
.ok_or_else(|| anyhow!("invalid signing key path"))?
|
|
||||||
.to_string_lossy();
|
|
||||||
fs::create_dir_all(key_dir)?;
|
|
||||||
self.preflight_container()?;
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("run")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/keys", key_dir.display()))
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg("/bin/sh")
|
|
||||||
.arg("-lc")
|
|
||||||
.arg(format!(
|
|
||||||
"openssl genrsa -out /keys/{key_name} 4096 && \
|
|
||||||
openssl rsa -in /keys/{key_name} -pubout -out /keys/{key_name}.pub"
|
|
||||||
))
|
|
||||||
.status()
|
|
||||||
.context("failed to run key generation command")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("key generation failed with {status}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rootfs(&self, root: &Path, packages: &[String]) -> Result<()> {
|
|
||||||
self.preflight_container()?;
|
|
||||||
let pubkey = self.abs_config_path(&self.config.signing_pubkey);
|
|
||||||
if !pubkey.exists() {
|
|
||||||
bail!("rootfs requires a configured public signing key; run `distro init-key` first");
|
|
||||||
}
|
|
||||||
fs::create_dir_all(root)?;
|
|
||||||
log::step(
|
|
||||||
"rootfs",
|
|
||||||
&format!(
|
|
||||||
"{} -> {} [{}]",
|
|
||||||
packages.join(", "),
|
|
||||||
root.display(),
|
|
||||||
root.display()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("run")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/rootfs", root.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/repo:ro", self.pkgs_root().display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!(
|
|
||||||
"{}:/etc/apk/keys/distro.rsa.pub:ro",
|
|
||||||
pubkey.display()
|
|
||||||
))
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg("apk")
|
|
||||||
.arg("--root")
|
|
||||||
.arg("/rootfs")
|
|
||||||
.arg("--keys-dir")
|
|
||||||
.arg("/etc/apk/keys")
|
|
||||||
.arg("--initdb")
|
|
||||||
.arg("--repository")
|
|
||||||
.arg("/repo")
|
|
||||||
.arg("add")
|
|
||||||
.args(packages)
|
|
||||||
.status()
|
|
||||||
.context("failed to run rootfs apk command")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("rootfs creation failed with {status}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- per-recipe build steps -------------------------------------------
|
|
||||||
|
|
||||||
fn build_host(&self, recipe: &Recipe, output: &OutputPackage, rebuild: bool) -> Result<()> {
|
|
||||||
let manifest = self.manifest_path(&output.key());
|
|
||||||
let hash = self.manifest_hash(recipe)?;
|
|
||||||
if !rebuild && fs::read_to_string(&manifest).ok().as_deref() == Some(hash.as_str()) {
|
|
||||||
log::skip("up-to-date", &output.key());
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
log::step(
|
|
||||||
"build",
|
|
||||||
&format!("{} {}-r{}", output.key(), recipe.version, recipe.revision),
|
|
||||||
);
|
|
||||||
let build_dir = self.host_build_dir(recipe);
|
|
||||||
let dest_dir = self.host_pkg_dir(recipe);
|
|
||||||
let source_dir = self.unpacked_source_dir(recipe);
|
|
||||||
// A rebuild starts from a clean build dir; sources stay shared.
|
|
||||||
if rebuild {
|
|
||||||
Self::recreate(&build_dir)?;
|
|
||||||
} else {
|
|
||||||
fs::create_dir_all(&build_dir)?;
|
|
||||||
}
|
|
||||||
Self::recreate(&dest_dir)?;
|
|
||||||
fs::create_dir_all(&build_dir)?;
|
|
||||||
|
|
||||||
let env = PhaseEnv {
|
|
||||||
source_dir: source_dir_env(recipe),
|
|
||||||
build_dir: C_BUILD,
|
|
||||||
dest_dir: C_DEST,
|
|
||||||
prefix: HOST_PREFIX,
|
|
||||||
sysroot: C_SYSROOT,
|
|
||||||
};
|
|
||||||
self.run_recipe_phases(
|
|
||||||
recipe,
|
|
||||||
output,
|
|
||||||
&env,
|
|
||||||
&source_dir,
|
|
||||||
&build_dir,
|
|
||||||
&dest_dir,
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
fs::create_dir_all(manifest.parent().unwrap())?;
|
|
||||||
fs::write(manifest, hash)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_target(&self, recipe: &Recipe, output: &OutputPackage, rebuild: bool) -> Result<()> {
|
|
||||||
let manifest = self.manifest_path(&output.key());
|
|
||||||
let hash = self.manifest_hash(recipe)?;
|
|
||||||
if !rebuild && fs::read_to_string(&manifest).ok().as_deref() == Some(hash.as_str()) {
|
|
||||||
log::skip("up-to-date", &output.key());
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
log::step(
|
|
||||||
"build",
|
|
||||||
&format!("{} {}-r{}", output.key(), recipe.version, recipe.revision),
|
|
||||||
);
|
|
||||||
let build_dir = self.build_dir(recipe);
|
|
||||||
let source_dir = self.unpacked_source_dir(recipe);
|
|
||||||
// A rebuild starts from a clean build dir; sources stay shared.
|
|
||||||
if rebuild {
|
|
||||||
Self::recreate(&build_dir)?;
|
|
||||||
} else {
|
|
||||||
fs::create_dir_all(&build_dir)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let dest_tmp =
|
|
||||||
tempfile::tempdir_in(&build_dir).context("failed to create ephemeral destdir")?;
|
|
||||||
let dest_dir = dest_tmp.path();
|
|
||||||
|
|
||||||
let sysroot = self.materialize_sysroot(recipe)?;
|
|
||||||
let env = PhaseEnv {
|
|
||||||
source_dir: source_dir_env(recipe),
|
|
||||||
build_dir: C_BUILD,
|
|
||||||
dest_dir: C_DEST,
|
|
||||||
prefix: TARGET_PREFIX,
|
|
||||||
sysroot: C_SYSROOT,
|
|
||||||
};
|
|
||||||
self.run_recipe_phases(
|
|
||||||
recipe,
|
|
||||||
output,
|
|
||||||
&env,
|
|
||||||
&source_dir,
|
|
||||||
&build_dir,
|
|
||||||
dest_dir,
|
|
||||||
sysroot.as_ref().map(|s| s.path()),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
self.apk_mkpkg(output, dest_dir)?;
|
|
||||||
|
|
||||||
fs::create_dir_all(manifest.parent().unwrap())?;
|
|
||||||
fs::write(manifest, hash)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_recipe_phases(
|
|
||||||
&self,
|
|
||||||
recipe: &Recipe,
|
|
||||||
output: &OutputPackage,
|
|
||||||
env: &PhaseEnv<'_>,
|
|
||||||
source_dir: &Path,
|
|
||||||
build_dir: &Path,
|
|
||||||
dest_dir: &Path,
|
|
||||||
target_sysroot: Option<&Path>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let host_sandbox = self.materialize_host_sandbox(recipe)?;
|
|
||||||
let recipe_already_built = self.built_recipes.borrow().contains(&recipe.key());
|
|
||||||
|
|
||||||
// configure/build/check run at most once per recipe per invocation.
|
|
||||||
let mut shared_phases: Vec<(&str, bool)> = Vec::new();
|
|
||||||
if !recipe_already_built {
|
|
||||||
if let Some(p) = recipe.configure_fn.as_deref() {
|
|
||||||
shared_phases.push((p, false));
|
|
||||||
}
|
|
||||||
if let Some(p) = recipe.build_fn.as_deref() {
|
|
||||||
shared_phases.push((p, false));
|
|
||||||
}
|
|
||||||
if !self.skip_checks {
|
|
||||||
if let Some(p) = recipe.check_fn.as_deref() {
|
|
||||||
shared_phases.push((p, false));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// install runs per output (each output has its own destdir/install_fn).
|
|
||||||
let install_phase = (output.install_fn.as_str(), true);
|
|
||||||
|
|
||||||
let mut commands: Vec<PhaseCommand> = Vec::new();
|
|
||||||
for (phase, takes_pkg) in shared_phases.iter().chain(std::iter::once(&install_phase)) {
|
|
||||||
let pkg = if *takes_pkg {
|
|
||||||
Some((output.name.as_str(), C_DEST))
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let owner = if *takes_pkg {
|
|
||||||
output.key()
|
|
||||||
} else {
|
|
||||||
recipe.key()
|
|
||||||
};
|
|
||||||
log::info("phase", &format!("{phase} {owner}"));
|
|
||||||
commands.extend(collect_phase_commands(
|
|
||||||
&recipe.path,
|
|
||||||
&self.repo,
|
|
||||||
&self.config,
|
|
||||||
phase,
|
|
||||||
env,
|
|
||||||
pkg,
|
|
||||||
)?);
|
|
||||||
}
|
|
||||||
if commands.is_empty() {
|
|
||||||
self.built_recipes.borrow_mut().insert(recipe.key());
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
self.run_in_container(
|
|
||||||
source_dir,
|
|
||||||
build_dir,
|
|
||||||
dest_dir,
|
|
||||||
host_sandbox.as_ref().map(|s| s.path()),
|
|
||||||
target_sysroot,
|
|
||||||
&commands,
|
|
||||||
)?;
|
|
||||||
self.built_recipes.borrow_mut().insert(recipe.key());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_in_container(
|
|
||||||
&self,
|
|
||||||
source_dir: &Path,
|
|
||||||
build_dir: &Path,
|
|
||||||
dest_dir: &Path,
|
|
||||||
host_sandbox: Option<&Path>,
|
|
||||||
target_sysroot: Option<&Path>,
|
|
||||||
commands: &[PhaseCommand],
|
|
||||||
) -> Result<()> {
|
|
||||||
fs::create_dir_all(dest_dir)?;
|
|
||||||
let mut process = Command::new(&self.config.container_runtime);
|
|
||||||
process
|
|
||||||
.arg("run")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:{C_SOURCE}:ro", source_dir.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:{C_BUILD}", build_dir.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:{C_DEST}", dest_dir.display()))
|
|
||||||
.arg("-w")
|
|
||||||
.arg(C_BUILD);
|
|
||||||
if let Some(sandbox) = host_sandbox {
|
|
||||||
// Host packages are configured with --prefix=/usr/local, so we
|
|
||||||
// bind the assembled tree exactly there (matching Jinx). This
|
|
||||||
// means rpaths, --print-search-dirs, pkg-config lookups, etc.
|
|
||||||
// all keep working with no extra environment fiddling.
|
|
||||||
process
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:{HOST_PREFIX}:ro", sandbox.display()));
|
|
||||||
}
|
|
||||||
if let Some(sysroot) = target_sysroot {
|
|
||||||
process
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:{C_SYSROOT}:ro", sysroot.display()))
|
|
||||||
.arg("-e")
|
|
||||||
.arg(format!("PKG_CONFIG_SYSROOT_DIR={C_SYSROOT}"))
|
|
||||||
.arg("-e")
|
|
||||||
.arg(format!("SYSROOT={C_SYSROOT}"));
|
|
||||||
}
|
|
||||||
process
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg("/bin/sh")
|
|
||||||
.arg("-c")
|
|
||||||
.arg(build_phase_script(commands));
|
|
||||||
let status = process
|
|
||||||
.status()
|
|
||||||
.context("failed to start container for phase commands")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("phase commands failed with {status}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apk_mkpkg(&self, output: &OutputPackage, dest_dir: &Path) -> Result<()> {
|
|
||||||
let repo = self.pkgs_dir();
|
|
||||||
fs::create_dir_all(&repo)?;
|
|
||||||
let signing_key = self.abs_config_path(&self.config.signing_key);
|
|
||||||
if !signing_key.exists() {
|
|
||||||
bail!("package signing key is missing; run `distro init-key` first");
|
|
||||||
}
|
|
||||||
log::step("package", &format!("{} -> {}", output.name, repo.display()));
|
|
||||||
let plan = apk::mkpkg_plan(&self.config, output, Path::new(C_DEST), Path::new("/out"));
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("run")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:{C_DEST}:ro", dest_dir.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/out", repo.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/keys/distro.rsa:ro", signing_key.display()))
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg("apk")
|
|
||||||
.arg("--sign-key")
|
|
||||||
.arg("/keys/distro.rsa")
|
|
||||||
.args(plan.args)
|
|
||||||
.status()
|
|
||||||
.context("failed to run apk mkpkg command")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("apk mkpkg failed with {status}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- source preparation (Jinx-style persistent sources/<recipe>/) ------
|
|
||||||
|
|
||||||
fn ensure_source(&self, recipe: &Recipe) -> Result<()> {
|
|
||||||
let cached = source::fetch_sources(recipe, &self.source_cache_dir())?;
|
|
||||||
|
|
||||||
let unpacked = self.unpacked_source_dir(recipe);
|
|
||||||
let version_stamp = self.source_stamp(recipe, "version");
|
|
||||||
let want_version = format!("{}-r{}", recipe.version, recipe.revision);
|
|
||||||
let need_extract =
|
|
||||||
fs::read_to_string(&version_stamp).ok().as_deref() != Some(&want_version);
|
|
||||||
|
|
||||||
if need_extract {
|
|
||||||
log::info("unpack", &recipe.key());
|
|
||||||
if unpacked.exists() {
|
|
||||||
fs::remove_dir_all(&unpacked)?;
|
|
||||||
}
|
|
||||||
fs::create_dir_all(&unpacked)?;
|
|
||||||
for (src, tarball) in recipe.sources.iter().zip(cached.iter()) {
|
|
||||||
let dest = if src.name.is_empty() {
|
|
||||||
unpacked.clone()
|
|
||||||
} else {
|
|
||||||
unpacked.join(&src.name)
|
|
||||||
};
|
|
||||||
self.extract_tarball(tarball, &dest, src.strip_components)?;
|
|
||||||
}
|
|
||||||
fs::write(&version_stamp, &want_version)?;
|
|
||||||
let patched = self.source_stamp(recipe, "patched");
|
|
||||||
let _ = fs::remove_file(&patched);
|
|
||||||
}
|
|
||||||
|
|
||||||
let patched_stamp = self.source_stamp(recipe, "patched");
|
|
||||||
if !patched_stamp.exists() {
|
|
||||||
self.apply_patches(recipe, &unpacked)?;
|
|
||||||
fs::write(&patched_stamp, "")?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extract_tarball(&self, tarball: &Path, dest: &Path, strip_components: u32) -> Result<()> {
|
|
||||||
fs::create_dir_all(dest)?;
|
|
||||||
log::info(
|
|
||||||
"extract",
|
|
||||||
&format!("{} -> {}", tarball.display(), dest.display()),
|
|
||||||
);
|
|
||||||
let strip = if strip_components > 0 {
|
|
||||||
format!("--strip-components={strip_components}")
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
};
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("run")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/in.tar:ro", tarball.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/out", dest.display()))
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg("/bin/sh")
|
|
||||||
.arg("-c")
|
|
||||||
.arg(format!("tar -xf /in.tar -C /out {strip}"))
|
|
||||||
.status()
|
|
||||||
.context("failed to unpack source archive")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("source unpack failed with {status}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn apply_patches(&self, recipe: &Recipe, source_dir: &Path) -> Result<()> {
|
|
||||||
let patches = patches::discover(&recipe.dir)?;
|
|
||||||
if patches.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if recipe.sources.iter().any(|s| !s.name.is_empty()) {
|
|
||||||
bail!(
|
|
||||||
"recipe `{}` has patches/ but uses multi-source layout; \
|
|
||||||
apply patches yourself via ctx.run in the configure phase",
|
|
||||||
recipe.id
|
|
||||||
);
|
|
||||||
}
|
|
||||||
for patch in patches {
|
|
||||||
log::info(
|
|
||||||
"patch",
|
|
||||||
&format!(
|
|
||||||
"{} <- {}",
|
|
||||||
recipe.id,
|
|
||||||
patch
|
|
||||||
.file_name()
|
|
||||||
.map(|n| n.to_string_lossy().into_owned())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("run")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/source", source_dir.display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/patch.diff:ro", patch.display()))
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg("patch")
|
|
||||||
.arg("-d")
|
|
||||||
.arg("/source")
|
|
||||||
.arg("-p1")
|
|
||||||
.arg("-i")
|
|
||||||
.arg("/patch.diff")
|
|
||||||
.status()
|
|
||||||
.context("failed to apply patch")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("patch {} failed with {status}", patch.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- host sandbox / target sysroot materialization --------------------
|
|
||||||
|
|
||||||
/// Assemble all `host_deps` into a single ephemeral tree that we can mount
|
|
||||||
/// at `/usr/local` inside the container. Following Jinx, we hard-link
|
|
||||||
/// rather than byte-copy so this is essentially free. The returned
|
|
||||||
/// `TempDir` cleans up when dropped.
|
|
||||||
fn materialize_host_sandbox(&self, recipe: &Recipe) -> Result<Option<tempfile::TempDir>> {
|
|
||||||
if recipe.host_deps.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
fs::create_dir_all(self.repo.join("build"))?;
|
|
||||||
let sandbox = tempfile::Builder::new()
|
|
||||||
.prefix(&format!("host-sandbox-{}-", recipe.id))
|
|
||||||
.tempdir_in(self.repo.join("build"))
|
|
||||||
.context("failed to create host sandbox tempdir")?;
|
|
||||||
log::info(
|
|
||||||
"host-sandbox",
|
|
||||||
&format!("{} <- [{}]", recipe.id, recipe.host_deps.join(", ")),
|
|
||||||
);
|
|
||||||
for dep in &recipe.host_deps {
|
|
||||||
let source = self.host_pkg_dir_by_id(dep).join("usr/local");
|
|
||||||
if !source.exists() {
|
|
||||||
bail!(
|
|
||||||
"host dependency `{dep}` has not been built at {}",
|
|
||||||
source.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
hardlink_tree(&source, sandbox.path())?;
|
|
||||||
}
|
|
||||||
Ok(Some(sandbox))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn materialize_sysroot(&self, recipe: &Recipe) -> Result<Option<tempfile::TempDir>> {
|
|
||||||
let mut deps: Vec<String> = recipe
|
|
||||||
.build_deps
|
|
||||||
.iter()
|
|
||||||
.chain(recipe.deps.iter())
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
deps.sort();
|
|
||||||
deps.dedup();
|
|
||||||
if deps.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
// The local repo index must be present and current so apk can resolve
|
|
||||||
// and verify the just-built target packages.
|
|
||||||
self.repo_index()?;
|
|
||||||
let pubkey = self.abs_config_path(&self.config.signing_pubkey);
|
|
||||||
if !pubkey.exists() {
|
|
||||||
bail!("target dependency sysroot requires a configured public signing key");
|
|
||||||
}
|
|
||||||
fs::create_dir_all(self.repo.join("build"))?;
|
|
||||||
let sysroot = tempfile::Builder::new()
|
|
||||||
.prefix(&format!("sysroot-{}-", recipe.id))
|
|
||||||
.tempdir_in(self.repo.join("build"))
|
|
||||||
.context("failed to create sysroot tempdir")?;
|
|
||||||
log::info(
|
|
||||||
"sysroot",
|
|
||||||
&format!("{} <- [{}]", recipe.id, deps.join(", ")),
|
|
||||||
);
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("run")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/sysroot", sysroot.path().display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!("{}:/repo:ro", self.pkgs_root().display()))
|
|
||||||
.arg("-v")
|
|
||||||
.arg(format!(
|
|
||||||
"{}:/etc/apk/keys/distro.rsa.pub:ro",
|
|
||||||
pubkey.display()
|
|
||||||
))
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg("apk")
|
|
||||||
.arg("--root")
|
|
||||||
.arg("/sysroot")
|
|
||||||
.arg("--keys-dir")
|
|
||||||
.arg("/etc/apk/keys")
|
|
||||||
.arg("--initdb")
|
|
||||||
.arg("--repository")
|
|
||||||
.arg("/repo")
|
|
||||||
.arg("add")
|
|
||||||
.args(&deps)
|
|
||||||
.status()
|
|
||||||
.context("failed to install target dependency sysroot")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("target dependency sysroot install failed with {status}");
|
|
||||||
}
|
|
||||||
Ok(Some(sysroot))
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- preflight & container image --------------------------------------
|
|
||||||
|
|
||||||
fn preflight_container(&self) -> Result<()> {
|
|
||||||
if self.preflight_done.get() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("--version")
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.status()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"{} is required but was not found",
|
|
||||||
self.config.container_runtime
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!(
|
|
||||||
"{} preflight failed with {status}",
|
|
||||||
self.config.container_runtime
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let dockerfile = self.abs_config_path(&self.config.container_dockerfile);
|
|
||||||
self.ensure_container_image(&dockerfile)?;
|
|
||||||
self.preflight_done.set(true);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_container_image(&self, dockerfile: &Path) -> Result<()> {
|
|
||||||
if !dockerfile.exists() {
|
|
||||||
bail!(
|
|
||||||
"configured container Dockerfile does not exist: {}",
|
|
||||||
dockerfile.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let hash = self.container_build_hash(dockerfile)?;
|
|
||||||
let stamp = self.repo.join("build/container-image.hash");
|
|
||||||
if fs::read_to_string(&stamp).ok().as_deref() == Some(hash.as_str())
|
|
||||||
&& self.container_image_exists()?
|
|
||||||
{
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
log::step(
|
|
||||||
"image",
|
|
||||||
&format!(
|
|
||||||
"building {} from {}",
|
|
||||||
self.config.container_image,
|
|
||||||
dockerfile.display()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("build")
|
|
||||||
.arg("-f")
|
|
||||||
.arg(dockerfile)
|
|
||||||
.arg("-t")
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg(&self.repo)
|
|
||||||
.status()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to build container image `{}` from {}",
|
|
||||||
self.config.container_image,
|
|
||||||
dockerfile.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!(
|
|
||||||
"container image build failed for `{}` with {status}",
|
|
||||||
self.config.container_image
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fs::create_dir_all(stamp.parent().unwrap())?;
|
|
||||||
fs::write(stamp, hash)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn container_image_exists(&self) -> Result<bool> {
|
|
||||||
let status = Command::new(&self.config.container_runtime)
|
|
||||||
.arg("image")
|
|
||||||
.arg("exists")
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.status()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to inspect container image `{}`",
|
|
||||||
self.config.container_image
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
Ok(status.success())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn container_build_hash(&self, dockerfile: &Path) -> Result<String> {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(fs::read(dockerfile)?);
|
|
||||||
hasher.update(self.config.container_image.as_bytes());
|
|
||||||
Ok(hex::encode(hasher.finalize()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn manifest_hash(&self, recipe: &Recipe) -> Result<String> {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(self.config.target_arch.as_bytes());
|
|
||||||
hasher.update(recipe.version.as_bytes());
|
|
||||||
hasher.update(recipe.revision.to_le_bytes());
|
|
||||||
for source in &recipe.sources {
|
|
||||||
hasher.update(source.url.as_bytes());
|
|
||||||
hasher.update(source.sha256.as_bytes());
|
|
||||||
}
|
|
||||||
for patch in patches::discover(&recipe.dir)? {
|
|
||||||
hasher.update(patch.display().to_string().as_bytes());
|
|
||||||
hasher.update(fs::read(patch)?);
|
|
||||||
}
|
|
||||||
Ok(hex::encode(hasher.finalize()))
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- path helpers -----------------------------------------------------
|
|
||||||
|
|
||||||
fn source_cache_dir(&self) -> PathBuf {
|
|
||||||
self.repo.join("build/cache/sources")
|
|
||||||
}
|
|
||||||
fn unpacked_source_dir(&self, recipe: &Recipe) -> PathBuf {
|
|
||||||
self.repo.join("build/sources").join(recipe.slug())
|
|
||||||
}
|
|
||||||
fn source_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf {
|
|
||||||
self.repo
|
|
||||||
.join("build/sources")
|
|
||||||
.join(format!("{}.{kind}", recipe.slug()))
|
|
||||||
}
|
|
||||||
fn build_dir(&self, recipe: &Recipe) -> PathBuf {
|
|
||||||
self.repo.join("build/builds").join(&recipe.id)
|
|
||||||
}
|
|
||||||
fn host_build_dir(&self, recipe: &Recipe) -> PathBuf {
|
|
||||||
self.repo.join("build/host-builds").join(&recipe.id)
|
|
||||||
}
|
|
||||||
fn host_pkg_dir(&self, recipe: &Recipe) -> PathBuf {
|
|
||||||
self.repo.join("build/host-pkgs").join(&recipe.id)
|
|
||||||
}
|
|
||||||
fn host_pkg_dir_by_id(&self, host_recipe_id: &str) -> PathBuf {
|
|
||||||
self.repo.join("build/host-pkgs").join(host_recipe_id)
|
|
||||||
}
|
|
||||||
/// Root of the target package repo. apk treats this as the repo root and
|
|
||||||
/// expects `<root>/<arch>/APKINDEX.tar.gz` underneath.
|
|
||||||
fn pkgs_root(&self) -> PathBuf {
|
|
||||||
self.repo.join("build/pkgs")
|
|
||||||
}
|
|
||||||
/// Arch-specific package directory: where .apk files and the index live.
|
|
||||||
fn pkgs_dir(&self) -> PathBuf {
|
|
||||||
self.pkgs_root().join(&self.config.target_arch)
|
|
||||||
}
|
|
||||||
fn manifest_path(&self, output_key: &str) -> PathBuf {
|
|
||||||
// Output keys may contain `:` (e.g. `host:gcc`); the manifest file
|
|
||||||
// name uses the filesystem-safe slug form instead.
|
|
||||||
let safe = output_key.replace(':', "-");
|
|
||||||
self.repo
|
|
||||||
.join("build/manifests")
|
|
||||||
.join(format!("{safe}.hash"))
|
|
||||||
}
|
|
||||||
fn abs_config_path(&self, path: &Path) -> PathBuf {
|
|
||||||
if path.is_absolute() {
|
|
||||||
path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
self.repo.join(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recreate(path: &Path) -> Result<()> {
|
|
||||||
if path.exists() {
|
|
||||||
fs::remove_dir_all(path)?;
|
|
||||||
}
|
|
||||||
fs::create_dir_all(path)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_phase_script(commands: &[PhaseCommand]) -> String {
|
|
||||||
let mut parts: Vec<String> = Vec::with_capacity(commands.len() + 1);
|
|
||||||
parts.push("set -e".to_owned());
|
|
||||||
for cmd in commands {
|
|
||||||
let mut tokens: Vec<String> = Vec::with_capacity(cmd.env.len() + cmd.argv.len());
|
|
||||||
for (k, v) in &cmd.env {
|
|
||||||
let value = shell_escape::unix::escape(v.as_str().into()).into_owned();
|
|
||||||
tokens.push(format!("{k}={value}"));
|
|
||||||
}
|
|
||||||
for arg in &cmd.argv {
|
|
||||||
tokens.push(shell_escape::unix::escape(arg.as_str().into()).into_owned());
|
|
||||||
}
|
|
||||||
parts.push(tokens.join(" "));
|
|
||||||
}
|
|
||||||
parts.join("\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Expose `/source` as a string for single-source recipes, or a struct of
|
|
||||||
/// `/source/<name>` paths for multi-source recipes.
|
|
||||||
fn source_dir_env(recipe: &Recipe) -> SourceDir {
|
|
||||||
if recipe.sources.iter().all(|s| s.name.is_empty()) {
|
|
||||||
SourceDir::Single(C_SOURCE.to_owned())
|
|
||||||
} else {
|
|
||||||
let map = recipe
|
|
||||||
.sources
|
|
||||||
.iter()
|
|
||||||
.map(|s| (s.name.clone(), format!("{C_SOURCE}/{}", s.name)))
|
|
||||||
.collect();
|
|
||||||
SourceDir::Many(map)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Mirror `src` into `dst` using hard links for regular files (and preserving
|
|
||||||
/// symlinks). This matches Jinx's `cp -Pplr`: no bytes are copied, just inode
|
|
||||||
/// references, so assembling a host-deps sandbox is essentially free.
|
|
||||||
fn hardlink_tree(src: &Path, dst: &Path) -> Result<()> {
|
|
||||||
fs::create_dir_all(dst)?;
|
|
||||||
for entry in walkdir::WalkDir::new(src) {
|
|
||||||
let entry = entry?;
|
|
||||||
let relative = entry.path().strip_prefix(src)?;
|
|
||||||
if relative.as_os_str().is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let target = dst.join(relative);
|
|
||||||
let file_type = entry.file_type();
|
|
||||||
if file_type.is_dir() {
|
|
||||||
fs::create_dir_all(&target)?;
|
|
||||||
} else if file_type.is_symlink() {
|
|
||||||
if let Some(parent) = target.parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
let link_target = fs::read_link(entry.path())?;
|
|
||||||
// Replace any pre-existing entry (multiple host deps may ship the
|
|
||||||
// same symlink path).
|
|
||||||
let _ = fs::remove_file(&target);
|
|
||||||
std::os::unix::fs::symlink(&link_target, &target)?;
|
|
||||||
} else if file_type.is_file() {
|
|
||||||
if let Some(parent) = target.parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
if target.exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
fs::hard_link(entry.path(), &target).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to hard-link {} -> {}",
|
|
||||||
entry.path().display(),
|
|
||||||
target.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
+283
@@ -0,0 +1,283 @@
|
|||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src import apk, fetch, log
|
||||||
|
from src.container import Container, Mount
|
||||||
|
from src.context import RecipeContext
|
||||||
|
from src.layout import Layout
|
||||||
|
from src.plan import (
|
||||||
|
PHASE_STAMPS,
|
||||||
|
Plan,
|
||||||
|
stamp_dir,
|
||||||
|
stamp_token,
|
||||||
|
stamp_valid,
|
||||||
|
transitive_host_deps,
|
||||||
|
)
|
||||||
|
from src.recipe import Recipe, RecipeSet
|
||||||
|
|
||||||
|
|
||||||
|
def _container_name(*parts: str) -> str:
|
||||||
|
name = "-".join(parts)
|
||||||
|
name = re.sub(
|
||||||
|
r"[^a-zA-Z0-9_.-]",
|
||||||
|
lambda m: f"_{ord(m.group(0)):02x}",
|
||||||
|
name,
|
||||||
|
)
|
||||||
|
if not name or not name[0].isalnum():
|
||||||
|
name = f"orchid-{name}"
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def _source_tree(layout: Layout, r: Recipe, key: str | None) -> Path:
|
||||||
|
base = layout.source_tree(r.name, r.version)
|
||||||
|
if len(r.sources) == 1:
|
||||||
|
return base
|
||||||
|
if key is None:
|
||||||
|
raise ValueError(f"{r.name}: multi-source entries must be named")
|
||||||
|
return base / key
|
||||||
|
|
||||||
|
|
||||||
|
def _prepare_all_sources(layout: Layout, r: Recipe) -> None:
|
||||||
|
for key, src in r.sources.items():
|
||||||
|
tree = _source_tree(layout, r, key)
|
||||||
|
tree.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
fetch.prepare_source(layout, r.dir, src, tree)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_path_env(host_deps_order: list[str], layout: Layout) -> str:
|
||||||
|
parts: list[str] = []
|
||||||
|
for h in reversed(host_deps_order):
|
||||||
|
parts += [
|
||||||
|
f"/tools/{h}/sbin",
|
||||||
|
f"/tools/{h}/bin",
|
||||||
|
]
|
||||||
|
parts += [
|
||||||
|
"/usr/local/sbin",
|
||||||
|
"/usr/local/bin",
|
||||||
|
"/usr/sbin",
|
||||||
|
"/usr/bin",
|
||||||
|
"/sbin",
|
||||||
|
"/bin",
|
||||||
|
]
|
||||||
|
return ":".join(parts)
|
||||||
|
|
||||||
|
|
||||||
|
def _container_for(
|
||||||
|
rs: RecipeSet, layout: Layout, profile: dict, r: Recipe
|
||||||
|
) -> tuple[Container, list[str]]:
|
||||||
|
host_order = transitive_host_deps(rs, r)
|
||||||
|
name = _container_name("orchid", layout.build.name, r.kind, r.name)
|
||||||
|
|
||||||
|
mounts: list[Mount] = []
|
||||||
|
tmpfs: list[str] = ["/sysroot"]
|
||||||
|
# sources
|
||||||
|
if len(r.sources) == 1:
|
||||||
|
key = next(iter(r.sources.keys()))
|
||||||
|
mounts.append(Mount(_source_tree(layout, r, key), "/sources", readonly=True))
|
||||||
|
else:
|
||||||
|
for k in r.sources.keys():
|
||||||
|
mounts.append(
|
||||||
|
Mount(_source_tree(layout, r, k), f"/sources/{k}", readonly=True)
|
||||||
|
)
|
||||||
|
# build dir
|
||||||
|
wd = layout.build_workdir(r.name if r.kind == "target" else f"host-{r.name}")
|
||||||
|
wd.mkdir(parents=True, exist_ok=True)
|
||||||
|
mounts.append(Mount(wd, "/build", readonly=False))
|
||||||
|
# files dir
|
||||||
|
if r.files_dir is not None:
|
||||||
|
mounts.append(Mount(r.files_dir, "/files", readonly=True))
|
||||||
|
# tools
|
||||||
|
for h in host_order:
|
||||||
|
mounts.append(Mount(layout.host_pkg_dir(h), f"/tools/{h}", readonly=True))
|
||||||
|
# pkgs (produced packages persist on disk); sysroot lives in tmpfs
|
||||||
|
mounts.append(Mount(layout.pkgs_dir, "/pkgs", readonly=False))
|
||||||
|
|
||||||
|
env = {
|
||||||
|
"PATH": _build_path_env(host_order, layout),
|
||||||
|
"ORCHID_ARCH": profile["arch"],
|
||||||
|
"ORCHID_TRIPLE": profile["triple"],
|
||||||
|
"ORCHID_JOBS": str(os.cpu_count() or 1),
|
||||||
|
}
|
||||||
|
c = Container(
|
||||||
|
name=name,
|
||||||
|
image=profile["container_image"],
|
||||||
|
mounts=mounts,
|
||||||
|
tmpfs=tmpfs,
|
||||||
|
network=False,
|
||||||
|
extra_env=env,
|
||||||
|
)
|
||||||
|
return c, host_order
|
||||||
|
|
||||||
|
|
||||||
|
def _recipe_ref(r: Recipe) -> str:
|
||||||
|
return f"{r.key} ({r.version}-r{r.revision})"
|
||||||
|
|
||||||
|
|
||||||
|
def _mark_stamp(layout: Layout, r: Recipe, phase: str) -> None:
|
||||||
|
name = PHASE_STAMPS.get(phase)
|
||||||
|
if name is None:
|
||||||
|
return
|
||||||
|
d = stamp_dir(layout, r)
|
||||||
|
d.mkdir(parents=True, exist_ok=True)
|
||||||
|
(d / name).write_text(stamp_token(r))
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_stamps(layout: Layout, r: Recipe) -> None:
|
||||||
|
d = stamp_dir(layout, r)
|
||||||
|
if d.is_dir():
|
||||||
|
shutil.rmtree(d)
|
||||||
|
|
||||||
|
|
||||||
|
def _wipe_workdir(layout: Layout, r: Recipe) -> None:
|
||||||
|
wd = layout.build_workdir(r.name if r.kind == "target" else f"host-{r.name}")
|
||||||
|
if wd.is_dir():
|
||||||
|
shutil.rmtree(wd)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_phases(ctx: RecipeContext, r: Recipe, layout: Layout) -> None:
|
||||||
|
for phase in ("prepare", "configure", "build"):
|
||||||
|
fn = r.phases.get(phase)
|
||||||
|
if fn is None:
|
||||||
|
continue
|
||||||
|
if stamp_valid(layout, r, phase):
|
||||||
|
log.info_field(phase, f"{_recipe_ref(r)} (cached)")
|
||||||
|
continue
|
||||||
|
log.info_field(phase, _recipe_ref(r))
|
||||||
|
fn(ctx)
|
||||||
|
_mark_stamp(layout, r, phase)
|
||||||
|
|
||||||
|
|
||||||
|
def _do_install(ctx: RecipeContext, r: Recipe) -> None:
|
||||||
|
fn = r.phases.get("install")
|
||||||
|
if fn is None:
|
||||||
|
return
|
||||||
|
log.info_field("install", _recipe_ref(r))
|
||||||
|
ctx._dest_output = r.name
|
||||||
|
try:
|
||||||
|
fn(ctx)
|
||||||
|
finally:
|
||||||
|
ctx._dest_output = None
|
||||||
|
|
||||||
|
|
||||||
|
def _split_subpackages(c: Container, r: Recipe) -> None:
|
||||||
|
for sub in r.subpackages:
|
||||||
|
c.exec(["mkdir", "-p", f"/dest/{sub.name}"])
|
||||||
|
for pat in sub.files:
|
||||||
|
c.exec_shell(apk.split_subpackage_script(r.name, sub.name, pat))
|
||||||
|
|
||||||
|
|
||||||
|
def _package_target(c: Container, r: Recipe, arch: str) -> None:
|
||||||
|
apk.mkpkg_base(c, r, arch)
|
||||||
|
for sub in r.subpackages:
|
||||||
|
apk.mkpkg_subpackage(c, r, sub, arch)
|
||||||
|
apk.index(c)
|
||||||
|
|
||||||
|
|
||||||
|
def _finalize_host(c: Container, layout: Layout, r: Recipe) -> None:
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
out = layout.host_pkg_dir(r.name)
|
||||||
|
if out.exists():
|
||||||
|
shutil.rmtree(out)
|
||||||
|
out.mkdir(parents=True)
|
||||||
|
p1 = subprocess.Popen(
|
||||||
|
[
|
||||||
|
"podman",
|
||||||
|
"exec",
|
||||||
|
c.name,
|
||||||
|
"sh",
|
||||||
|
"-c",
|
||||||
|
f"cd /dest/{r.name}/tools/{r.name} && tar -cf - .",
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
p2 = subprocess.Popen(["tar", "-xf", "-", "-C", str(out)], stdin=p1.stdout)
|
||||||
|
if p1.stdout is not None:
|
||||||
|
p1.stdout.close()
|
||||||
|
rc = p2.wait()
|
||||||
|
p1.wait()
|
||||||
|
if rc != 0 or p1.returncode != 0:
|
||||||
|
raise RuntimeError(f"host {r.name}: copy-out failed")
|
||||||
|
layout.host_pkg_marker(r.name, r.version, r.revision).write_text("ok\n")
|
||||||
|
|
||||||
|
|
||||||
|
def _sysroot_sync(c: Container, r: Recipe) -> None:
|
||||||
|
direct_deps = list(r.deps)
|
||||||
|
if not direct_deps:
|
||||||
|
return
|
||||||
|
initdb = not apk.sysroot_initialized(c)
|
||||||
|
apk.sysroot_install(c, direct_deps, initdb=initdb)
|
||||||
|
|
||||||
|
|
||||||
|
def build_one(
|
||||||
|
rs: RecipeSet, layout: Layout, profile: dict, r: Recipe, *, forced: bool = False
|
||||||
|
) -> None:
|
||||||
|
log.info_field("recipe", _recipe_ref(r))
|
||||||
|
if forced:
|
||||||
|
_clear_stamps(layout, r)
|
||||||
|
_wipe_workdir(layout, r)
|
||||||
|
_prepare_all_sources(layout, r)
|
||||||
|
|
||||||
|
c, _host_order = _container_for(rs, layout, profile, r)
|
||||||
|
c.start()
|
||||||
|
try:
|
||||||
|
# Ensure base output dest dir exists.
|
||||||
|
c.exec(["mkdir", "-p", f"/dest/{r.name}"])
|
||||||
|
|
||||||
|
_sysroot_sync(c, r)
|
||||||
|
|
||||||
|
ctx = RecipeContext(
|
||||||
|
recipe=r, profile=profile, container=c, jobs=os.cpu_count() or 1
|
||||||
|
)
|
||||||
|
_run_phases(ctx, r, layout)
|
||||||
|
_do_install(ctx, r)
|
||||||
|
|
||||||
|
if r.kind == "target":
|
||||||
|
log.info_field("package", _recipe_ref(r))
|
||||||
|
_split_subpackages(c, r)
|
||||||
|
_package_target(c, r, profile["arch"])
|
||||||
|
else:
|
||||||
|
log.info_field("finalize", _recipe_ref(r))
|
||||||
|
_finalize_host(c, layout, r)
|
||||||
|
finally:
|
||||||
|
c.stop()
|
||||||
|
log.ok_field("done", _recipe_ref(r))
|
||||||
|
|
||||||
|
|
||||||
|
def execute(plan: Plan, rs: RecipeSet, layout: Layout, profile: dict) -> None:
|
||||||
|
if not plan.order:
|
||||||
|
log.info_field("plan", "nothing to do")
|
||||||
|
return
|
||||||
|
for k in plan.order:
|
||||||
|
r = plan.recipes[k]
|
||||||
|
build_one(rs, layout, profile, r, forced=k in plan.forced)
|
||||||
|
|
||||||
|
|
||||||
|
def install_to(
|
||||||
|
layout: Layout,
|
||||||
|
profile: dict,
|
||||||
|
dest: Path,
|
||||||
|
pkgs: list[str],
|
||||||
|
*,
|
||||||
|
initdb: bool = True,
|
||||||
|
) -> None:
|
||||||
|
if not pkgs:
|
||||||
|
log.info_field("install", "nothing to install")
|
||||||
|
return
|
||||||
|
c = Container(
|
||||||
|
name=_container_name("orchid", layout.build.name, "install"),
|
||||||
|
image=profile["container_image"],
|
||||||
|
mounts=[
|
||||||
|
Mount(layout.pkgs_dir, "/pkgs", readonly=True),
|
||||||
|
Mount(dest, "/sysroot", readonly=False),
|
||||||
|
],
|
||||||
|
network=False,
|
||||||
|
)
|
||||||
|
c.start()
|
||||||
|
try:
|
||||||
|
log.info_field("install", f"{dest}: {', '.join(pkgs)}")
|
||||||
|
apk.sysroot_install(c, pkgs, initdb=initdb)
|
||||||
|
finally:
|
||||||
|
c.stop()
|
||||||
+240
@@ -0,0 +1,240 @@
|
|||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src import (
|
||||||
|
builder,
|
||||||
|
container,
|
||||||
|
log,
|
||||||
|
plan,
|
||||||
|
profile as profile_mod,
|
||||||
|
recipe as recipe_mod,
|
||||||
|
)
|
||||||
|
from src.layout import Layout, find_repo_root
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_build_dir(p: str | None) -> Path:
|
||||||
|
if p:
|
||||||
|
return Path(p).resolve()
|
||||||
|
env = os.environ.get("ORCHID_BUILD")
|
||||||
|
if env:
|
||||||
|
return Path(env).resolve()
|
||||||
|
cwd = Path.cwd()
|
||||||
|
if (cwd / "profile").is_symlink() or (cwd / "profile").exists():
|
||||||
|
return cwd
|
||||||
|
raise SystemExit("error: -C <build-dir> required (or run inside a build dir)")
|
||||||
|
|
||||||
|
|
||||||
|
def _layout(build_dir: Path) -> Layout:
|
||||||
|
repo = find_repo_root(build_dir)
|
||||||
|
return Layout(repo=repo, build=build_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _load(layout: Layout):
|
||||||
|
prof = profile_mod.load_profile(layout)
|
||||||
|
rs = recipe_mod.load_recipes(layout, prof)
|
||||||
|
return prof, rs
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_init(args) -> int:
|
||||||
|
target = Path(args.build_dir).resolve()
|
||||||
|
repo = find_repo_root(Path.cwd())
|
||||||
|
profile_mod.init_build_dir(target, repo, args.profile)
|
||||||
|
log.ok(f"initialized {target} (profile: {args.profile})")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_image(args) -> int:
|
||||||
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
||||||
|
layout.ensure()
|
||||||
|
prof = profile_mod.load_profile(layout)
|
||||||
|
container.ensure_image(
|
||||||
|
layout.dockerfile, prof["container_image"], layout.image_hash_file
|
||||||
|
)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _recipe_version(r) -> str:
|
||||||
|
return f"{r.version}-r{r.revision}"
|
||||||
|
|
||||||
|
|
||||||
|
def _print_plan(p: plan.Plan) -> None:
|
||||||
|
out = sys.stdout
|
||||||
|
if not p.order:
|
||||||
|
print(
|
||||||
|
f"{log.tag('info', stream=out)} "
|
||||||
|
f"{log.field('plan', 'nothing to do', stream=out)}"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
count = len(p.order)
|
||||||
|
suffix = "" if count == 1 else "s"
|
||||||
|
print(
|
||||||
|
f"{log.tag('info', stream=out)} "
|
||||||
|
f"{log.field('plan', f'{count} recipe{suffix}', stream=out)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
rows: list[tuple[str, str, str, str]] = []
|
||||||
|
for i, k in enumerate(p.order, start=1):
|
||||||
|
r = p.recipes[k]
|
||||||
|
rows.append((str(i), k, _recipe_version(r), ", ".join(p.stages.get(k, ()))))
|
||||||
|
|
||||||
|
widths = [
|
||||||
|
max(len(row[0]) for row in rows),
|
||||||
|
max(len("recipe"), *(len(row[1]) for row in rows)),
|
||||||
|
max(len("version"), *(len(row[2]) for row in rows)),
|
||||||
|
]
|
||||||
|
header = (
|
||||||
|
f" {'#':>{widths[0]}} "
|
||||||
|
f"{'recipe':<{widths[1]}} "
|
||||||
|
f"{'version':<{widths[2]}} "
|
||||||
|
"stages"
|
||||||
|
)
|
||||||
|
print(log.bold(header, stream=out))
|
||||||
|
for num, name, version, stages in rows:
|
||||||
|
print(
|
||||||
|
f" {num:>{widths[0]}} "
|
||||||
|
f"{name:<{widths[1]}} "
|
||||||
|
f"{version:<{widths[2]}} "
|
||||||
|
f"{stages}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_plan(args) -> int:
|
||||||
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
||||||
|
layout.ensure()
|
||||||
|
prof, rs = _load(layout)
|
||||||
|
p = plan.build_plan(rs, layout, args.recipes or None, rebuild=args.rebuild)
|
||||||
|
_print_plan(p)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_build(args) -> int:
|
||||||
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
||||||
|
layout.ensure()
|
||||||
|
prof, rs = _load(layout)
|
||||||
|
container.ensure_image(
|
||||||
|
layout.dockerfile, prof["container_image"], layout.image_hash_file
|
||||||
|
)
|
||||||
|
p = plan.build_plan(rs, layout, args.recipes or None, rebuild=args.rebuild)
|
||||||
|
if args.dry_run:
|
||||||
|
return cmd_plan(args)
|
||||||
|
builder.execute(p, rs, layout, prof)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_install(args) -> int:
|
||||||
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
||||||
|
layout.ensure()
|
||||||
|
prof, rs = _load(layout)
|
||||||
|
container.ensure_image(
|
||||||
|
layout.dockerfile, prof["container_image"], layout.image_hash_file
|
||||||
|
)
|
||||||
|
dest = Path(args.dest).resolve()
|
||||||
|
dest.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if args.recipes:
|
||||||
|
pkgs: list[str] = []
|
||||||
|
for k in args.recipes:
|
||||||
|
r = rs.get(k)
|
||||||
|
if r.kind != "target":
|
||||||
|
raise SystemExit(f"error: cannot install host recipe {k!r}")
|
||||||
|
pkgs.extend(r.outputs)
|
||||||
|
else:
|
||||||
|
pkgs = [r.name for r in rs.target.values() if r.enabled]
|
||||||
|
|
||||||
|
builder.install_to(layout, prof, dest, pkgs, initdb=args.initdb)
|
||||||
|
log.ok(f"installed {len(pkgs)} package(s) to {dest}")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_fetch(args) -> int:
|
||||||
|
layout = _layout(_resolve_build_dir(args.build_dir))
|
||||||
|
layout.ensure()
|
||||||
|
prof, rs = _load(layout)
|
||||||
|
from . import fetch as fetch_mod
|
||||||
|
|
||||||
|
targets = args.recipes or [r.key for r in rs.all() if r.enabled]
|
||||||
|
for k in targets:
|
||||||
|
r = rs.get(k)
|
||||||
|
for key, src in r.sources.items():
|
||||||
|
fetch_mod.fetch(layout, src)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def _common(p: argparse.ArgumentParser, with_recipes: bool = True) -> None:
|
||||||
|
p.add_argument(
|
||||||
|
"-C", "--build-dir", help="build directory (defaults to $ORCHID_BUILD or cwd)"
|
||||||
|
)
|
||||||
|
if with_recipes:
|
||||||
|
p.add_argument(
|
||||||
|
"recipes", nargs="*", help="recipe keys (target name or host:<name>)"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_parser() -> argparse.ArgumentParser:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog="orchid", description="Orchid distribution builder"
|
||||||
|
)
|
||||||
|
sub = parser.add_subparsers(dest="cmd", required=True)
|
||||||
|
|
||||||
|
p_init = sub.add_parser("init", help="create a build directory")
|
||||||
|
p_init.add_argument("build_dir")
|
||||||
|
p_init.add_argument("--profile", required=True)
|
||||||
|
p_init.set_defaults(func=cmd_init)
|
||||||
|
|
||||||
|
p_img = sub.add_parser("image", help="build/refresh the container image")
|
||||||
|
_common(p_img, with_recipes=False)
|
||||||
|
p_img.set_defaults(func=cmd_image)
|
||||||
|
|
||||||
|
p_plan = sub.add_parser("plan", help="print build plan")
|
||||||
|
_common(p_plan)
|
||||||
|
p_plan.add_argument("--rebuild", action="store_true")
|
||||||
|
p_plan.set_defaults(func=cmd_plan)
|
||||||
|
|
||||||
|
p_build = sub.add_parser("build", help="build recipes")
|
||||||
|
_common(p_build)
|
||||||
|
p_build.add_argument("--rebuild", action="store_true")
|
||||||
|
p_build.add_argument("-n", "--dry-run", action="store_true")
|
||||||
|
p_build.set_defaults(func=cmd_build)
|
||||||
|
|
||||||
|
p_inst = sub.add_parser(
|
||||||
|
"install", help="install built packages into a sysroot directory"
|
||||||
|
)
|
||||||
|
p_inst.add_argument(
|
||||||
|
"-C", "--build-dir", help="build directory (defaults to $ORCHID_BUILD or cwd)"
|
||||||
|
)
|
||||||
|
p_inst.add_argument("dest", help="destination sysroot directory")
|
||||||
|
p_inst.add_argument(
|
||||||
|
"recipes",
|
||||||
|
nargs="*",
|
||||||
|
help="target recipes to install (defaults to all enabled targets)",
|
||||||
|
)
|
||||||
|
p_inst.add_argument(
|
||||||
|
"--no-initdb",
|
||||||
|
dest="initdb",
|
||||||
|
action="store_false",
|
||||||
|
help="do not initialize the apk database (use for incremental installs)",
|
||||||
|
)
|
||||||
|
p_inst.set_defaults(func=cmd_install, initdb=True)
|
||||||
|
|
||||||
|
p_fetch = sub.add_parser("fetch", help="fetch sources only")
|
||||||
|
_common(p_fetch)
|
||||||
|
p_fetch.set_defaults(func=cmd_fetch)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = make_parser()
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
try:
|
||||||
|
return args.func(args)
|
||||||
|
except (SystemExit, KeyboardInterrupt):
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"{type(e).__name__}: {e}")
|
||||||
|
if os.environ.get("ORCHID_DEBUG"):
|
||||||
|
raise
|
||||||
|
return 1
|
||||||
-119
@@ -1,119 +0,0 @@
|
|||||||
use crate::build::Builder;
|
|
||||||
use crate::config::Config;
|
|
||||||
use crate::graph::PackageGraph;
|
|
||||||
use crate::recipe::RecipeSet;
|
|
||||||
use anyhow::{Context, Result, bail};
|
|
||||||
use clap::{Parser, Subcommand};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
|
||||||
#[command(name = "distro", version)]
|
|
||||||
pub struct Cli {
|
|
||||||
#[arg(long, default_value = ".")]
|
|
||||||
pub repo: PathBuf,
|
|
||||||
|
|
||||||
#[arg(long)]
|
|
||||||
pub skip_checks: bool,
|
|
||||||
|
|
||||||
#[command(subcommand)]
|
|
||||||
pub command: Command,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Subcommand)]
|
|
||||||
pub enum Command {
|
|
||||||
Build {
|
|
||||||
package: Option<String>,
|
|
||||||
},
|
|
||||||
Rebuild {
|
|
||||||
package: Option<String>,
|
|
||||||
},
|
|
||||||
Fetch {
|
|
||||||
package: String,
|
|
||||||
},
|
|
||||||
Graph {
|
|
||||||
package: Option<String>,
|
|
||||||
},
|
|
||||||
List,
|
|
||||||
Info {
|
|
||||||
package: String,
|
|
||||||
},
|
|
||||||
Clean,
|
|
||||||
Shell {
|
|
||||||
package: String,
|
|
||||||
},
|
|
||||||
RepoIndex,
|
|
||||||
InitKey,
|
|
||||||
Rootfs {
|
|
||||||
root: PathBuf,
|
|
||||||
packages: Vec<String>,
|
|
||||||
},
|
|
||||||
Update {
|
|
||||||
#[arg(long)]
|
|
||||||
bump: bool,
|
|
||||||
packages: Vec<String>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(cli: Cli) -> Result<()> {
|
|
||||||
let repo = cli.repo.canonicalize().unwrap_or(cli.repo);
|
|
||||||
match &cli.command {
|
|
||||||
Command::Clean => {
|
|
||||||
let build = repo.join("build");
|
|
||||||
if build.exists() {
|
|
||||||
fs::remove_dir_all(&build)
|
|
||||||
.with_context(|| format!("failed to remove {}", build.display()))?;
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Command::InitKey => {
|
|
||||||
let config = Config::load(&repo.join("config.star"))?;
|
|
||||||
Builder::new(repo, config, cli.skip_checks).init_key()
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let config = Config::load(&repo.join("config.star"))?;
|
|
||||||
let recipes = RecipeSet::load(&repo, &config)?;
|
|
||||||
if let Command::Update { bump, packages } = &cli.command {
|
|
||||||
return crate::update::run(&recipes, packages, *bump);
|
|
||||||
}
|
|
||||||
let graph = PackageGraph::new(&recipes)?;
|
|
||||||
let builder = Builder::new(repo, config, cli.skip_checks);
|
|
||||||
match cli.command {
|
|
||||||
Command::Build { package } => {
|
|
||||||
builder.build(&recipes, &graph, package.as_deref(), false)
|
|
||||||
}
|
|
||||||
Command::Rebuild { package } => {
|
|
||||||
builder.build(&recipes, &graph, package.as_deref(), true)
|
|
||||||
}
|
|
||||||
Command::Fetch { package } => builder.fetch(&recipes, &package),
|
|
||||||
Command::Graph { package } => {
|
|
||||||
for line in graph.render(package.as_deref())? {
|
|
||||||
println!("{line}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Command::List => {
|
|
||||||
for output in graph.outputs() {
|
|
||||||
println!("{output}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Command::Info { package } => {
|
|
||||||
let output = graph.output(&package)?;
|
|
||||||
println!("{}", serde_json::to_string_pretty(output)?);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
Command::Shell { package } => builder.shell(&recipes, &graph, &package),
|
|
||||||
Command::RepoIndex => builder.repo_index(),
|
|
||||||
Command::Rootfs { root, packages } => {
|
|
||||||
if packages.is_empty() {
|
|
||||||
bail!("rootfs requires at least one package");
|
|
||||||
}
|
|
||||||
builder.rootfs(&root, &packages)
|
|
||||||
}
|
|
||||||
Command::Update { .. } => unreachable!(),
|
|
||||||
Command::Clean | Command::InitKey => unreachable!(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
use crate::starlark::{eval_file, get_json_map, get_string, get_string_default};
|
|
||||||
use anyhow::{Context, Result};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct Config {
|
|
||||||
pub target_arch: String,
|
|
||||||
pub options: BTreeMap<String, JsonValue>,
|
|
||||||
pub container_runtime: String,
|
|
||||||
pub container_image: String,
|
|
||||||
pub container_dockerfile: PathBuf,
|
|
||||||
pub signing_key: PathBuf,
|
|
||||||
pub signing_pubkey: PathBuf,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn load(path: &Path) -> Result<Self> {
|
|
||||||
let module = eval_file(path, None, None)
|
|
||||||
.with_context(|| format!("failed to evaluate {}", path.display()))?;
|
|
||||||
Ok(Self {
|
|
||||||
target_arch: get_string(&module, "target_arch")?,
|
|
||||||
options: get_json_map(&module, "options")?,
|
|
||||||
container_runtime: get_string_default(&module, "container_runtime", "podman")?,
|
|
||||||
container_image: get_string(&module, "container_image")?,
|
|
||||||
container_dockerfile: PathBuf::from(get_string_default(
|
|
||||||
&module,
|
|
||||||
"container_dockerfile",
|
|
||||||
"Dockerfile",
|
|
||||||
)?),
|
|
||||||
signing_key: PathBuf::from(get_string(&module, "signing_key")?),
|
|
||||||
signing_pubkey: PathBuf::from(get_string(&module, "signing_pubkey")?),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
import atexit
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src import log
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Mount:
|
||||||
|
src: Path
|
||||||
|
dst: str
|
||||||
|
readonly: bool = False
|
||||||
|
|
||||||
|
def as_arg(self) -> str:
|
||||||
|
flag = ":ro" if self.readonly else ""
|
||||||
|
return f"{self.src}:{self.dst}{flag}"
|
||||||
|
|
||||||
|
|
||||||
|
_active: set["Container"] = set()
|
||||||
|
_lock = threading.Lock()
|
||||||
|
_handlers_installed = False
|
||||||
|
|
||||||
|
|
||||||
|
def _install_handlers() -> None:
|
||||||
|
global _handlers_installed
|
||||||
|
if _handlers_installed:
|
||||||
|
return
|
||||||
|
_handlers_installed = True
|
||||||
|
|
||||||
|
def cleanup(*_):
|
||||||
|
_shutdown_all()
|
||||||
|
|
||||||
|
atexit.register(_shutdown_all)
|
||||||
|
for s in (signal.SIGINT, signal.SIGTERM):
|
||||||
|
try:
|
||||||
|
signal.signal(s, lambda *_: (_shutdown_all(), os._exit(130)))
|
||||||
|
except (ValueError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _shutdown_all() -> None:
|
||||||
|
with _lock:
|
||||||
|
cs = list(_active)
|
||||||
|
_active.clear()
|
||||||
|
for c in cs:
|
||||||
|
try:
|
||||||
|
subprocess.run(
|
||||||
|
["podman", "rm", "-f", c.name],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Container:
|
||||||
|
name: str
|
||||||
|
image: str
|
||||||
|
mounts: list[Mount] = field(default_factory=list)
|
||||||
|
tmpfs: list[str] = field(default_factory=list)
|
||||||
|
network: bool = False
|
||||||
|
extra_env: dict[str, str] = field(default_factory=dict)
|
||||||
|
started: bool = False
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
return id(self)
|
||||||
|
|
||||||
|
def __eq__(self, other) -> bool:
|
||||||
|
return self is other
|
||||||
|
|
||||||
|
def start(self) -> None:
|
||||||
|
if self.started:
|
||||||
|
return
|
||||||
|
_install_handlers()
|
||||||
|
# Ensure no stale container.
|
||||||
|
subprocess.run(
|
||||||
|
["podman", "rm", "-f", self.name],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
argv = [
|
||||||
|
"podman",
|
||||||
|
"run",
|
||||||
|
"-d",
|
||||||
|
"--rm",
|
||||||
|
"--name",
|
||||||
|
self.name,
|
||||||
|
]
|
||||||
|
for t in ("/tmp", "/dest", "/var/cache", *self.tmpfs):
|
||||||
|
argv += ["--tmpfs", t]
|
||||||
|
argv += ["--network=none"] if not self.network else []
|
||||||
|
for m in self.mounts:
|
||||||
|
m.src.mkdir(parents=True, exist_ok=True) if not m.src.exists() else None
|
||||||
|
argv += ["-v", m.as_arg()]
|
||||||
|
argv += [self.image, "sleep", "infinity"]
|
||||||
|
log.debug(f"container start: {' '.join(argv)}")
|
||||||
|
r = subprocess.run(argv, capture_output=True, text=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError(f"podman run failed: {r.stderr.strip()}")
|
||||||
|
with _lock:
|
||||||
|
_active.add(self)
|
||||||
|
self.started = True
|
||||||
|
|
||||||
|
def stop(self) -> None:
|
||||||
|
if not self.started:
|
||||||
|
return
|
||||||
|
subprocess.run(
|
||||||
|
["podman", "rm", "-f", self.name],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
with _lock:
|
||||||
|
_active.discard(self)
|
||||||
|
self.started = False
|
||||||
|
|
||||||
|
def exec(
|
||||||
|
self,
|
||||||
|
argv: list[str],
|
||||||
|
*,
|
||||||
|
env: dict[str, str] | None = None,
|
||||||
|
cwd: str | None = None,
|
||||||
|
check: bool = True,
|
||||||
|
user: str | None = None,
|
||||||
|
) -> int:
|
||||||
|
if not self.started:
|
||||||
|
raise RuntimeError("container not started")
|
||||||
|
cmd = ["podman", "exec"]
|
||||||
|
if cwd:
|
||||||
|
cmd += ["-w", cwd]
|
||||||
|
if user:
|
||||||
|
cmd += ["-u", user]
|
||||||
|
merged: dict[str, str] = {}
|
||||||
|
merged.update(self.extra_env)
|
||||||
|
if env:
|
||||||
|
merged.update(env)
|
||||||
|
for k, v in merged.items():
|
||||||
|
cmd += ["-e", f"{k}={v}"]
|
||||||
|
cmd += [self.name, *argv]
|
||||||
|
log.debug(f"exec: {' '.join(argv)}")
|
||||||
|
rc = subprocess.call(cmd)
|
||||||
|
if check and rc != 0:
|
||||||
|
raise RuntimeError(f"command failed (exit {rc}): {' '.join(argv)}")
|
||||||
|
return rc
|
||||||
|
|
||||||
|
def exec_shell(
|
||||||
|
self,
|
||||||
|
script: str,
|
||||||
|
*,
|
||||||
|
env: dict[str, str] | None = None,
|
||||||
|
cwd: str | None = None,
|
||||||
|
check: bool = True,
|
||||||
|
) -> int:
|
||||||
|
return self.exec(["sh", "-ec", script], env=env, cwd=cwd, check=check)
|
||||||
|
|
||||||
|
def cp_out(self, src_in: str, dst_host: Path) -> None:
|
||||||
|
dst_host.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
r = subprocess.run(
|
||||||
|
["podman", "cp", f"{self.name}:{src_in}", str(dst_host)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError(f"podman cp failed: {r.stderr.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def hash_dockerfile(p: Path) -> str:
|
||||||
|
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def image_exists(tag: str) -> bool:
|
||||||
|
r = subprocess.run(["podman", "image", "exists", tag])
|
||||||
|
return r.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def build_image(dockerfile: Path, tag: str) -> None:
|
||||||
|
log.info(f"building container image {tag}")
|
||||||
|
r = subprocess.run(
|
||||||
|
["podman", "build", "-t", tag, "-f", str(dockerfile), str(dockerfile.parent)]
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError("podman build failed")
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_image(dockerfile: Path, tag: str, hash_file: Path) -> None:
|
||||||
|
cur = hash_dockerfile(dockerfile) + "\n" + tag
|
||||||
|
if hash_file.exists() and hash_file.read_text() == cur and image_exists(tag):
|
||||||
|
log.debug(f"image {tag} up-to-date")
|
||||||
|
return
|
||||||
|
build_image(dockerfile, tag)
|
||||||
|
hash_file.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
hash_file.write_text(cur)
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from pathlib import PurePosixPath
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from src.container import Container
|
||||||
|
from src.recipe import Recipe
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RecipeContext:
|
||||||
|
"""The `self` value passed to recipe phase functions."""
|
||||||
|
|
||||||
|
recipe: Recipe
|
||||||
|
profile: dict
|
||||||
|
container: Container
|
||||||
|
jobs: int
|
||||||
|
_dest_output: str | None = None
|
||||||
|
env: dict[str, str] = field(default_factory=dict)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
return self.recipe.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def version(self) -> str:
|
||||||
|
return self.recipe.version
|
||||||
|
|
||||||
|
@property
|
||||||
|
def revision(self) -> int:
|
||||||
|
return self.recipe.revision
|
||||||
|
|
||||||
|
@property
|
||||||
|
def source_dir(self) -> PurePosixPath:
|
||||||
|
srcs = self.recipe.sources
|
||||||
|
if len(srcs) == 1:
|
||||||
|
return PurePosixPath("/sources")
|
||||||
|
raise RuntimeError(f"{self.name}: multiple sources; use self.sources")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sources(self) -> dict[str, PurePosixPath]:
|
||||||
|
srcs = self.recipe.sources
|
||||||
|
if len(srcs) == 1:
|
||||||
|
raise RuntimeError(f"{self.name}: only one source; use self.source_dir")
|
||||||
|
out: dict[str, PurePosixPath] = {}
|
||||||
|
for k in srcs.keys():
|
||||||
|
if k is None:
|
||||||
|
raise RuntimeError(f"{self.name}: multiple sources must be named")
|
||||||
|
out[k] = PurePosixPath("/sources") / k
|
||||||
|
return out
|
||||||
|
|
||||||
|
@property
|
||||||
|
def build_dir(self) -> PurePosixPath:
|
||||||
|
return PurePosixPath("/build")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sysroot(self) -> PurePosixPath:
|
||||||
|
return PurePosixPath("/sysroot")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dest_dir(self) -> PurePosixPath:
|
||||||
|
if self._dest_output is None:
|
||||||
|
raise RuntimeError("dest_dir only available during install/package phases")
|
||||||
|
return PurePosixPath("/dest") / self._dest_output
|
||||||
|
|
||||||
|
@property
|
||||||
|
def files(self) -> PurePosixPath:
|
||||||
|
if self.recipe.files_dir is None:
|
||||||
|
raise RuntimeError(f"{self.name}: no files/ dir")
|
||||||
|
return PurePosixPath("/files")
|
||||||
|
|
||||||
|
@property
|
||||||
|
def prefix(self) -> str:
|
||||||
|
return f"/tools/{self.name}" if self.recipe.kind == "host" else "/usr"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def triple(self) -> str:
|
||||||
|
return self.profile["triple"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def arch(self) -> str:
|
||||||
|
return self.profile["arch"]
|
||||||
|
|
||||||
|
def run(
|
||||||
|
self,
|
||||||
|
*argv,
|
||||||
|
env: dict[str, str] | None = None,
|
||||||
|
cwd: str | os.PathLike | None = None,
|
||||||
|
) -> None:
|
||||||
|
flat: list[str] = []
|
||||||
|
for a in argv:
|
||||||
|
if isinstance(a, (PurePosixPath, os.PathLike)):
|
||||||
|
flat.append(str(a))
|
||||||
|
elif isinstance(a, bool):
|
||||||
|
raise TypeError("bool not allowed in run(); use str")
|
||||||
|
elif isinstance(a, (str, int)):
|
||||||
|
flat.append(str(a))
|
||||||
|
else:
|
||||||
|
raise TypeError(f"unsupported arg type in run(): {type(a).__name__}")
|
||||||
|
merged = dict(self.env)
|
||||||
|
if env:
|
||||||
|
merged.update(env)
|
||||||
|
cwd_s = str(cwd) if cwd is not None else "/build"
|
||||||
|
self.container.exec(flat, env=merged, cwd=cwd_s)
|
||||||
+179
@@ -0,0 +1,179 @@
|
|||||||
|
import contextlib
|
||||||
|
import fcntl
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tarfile
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src import log
|
||||||
|
from src.layout import Layout
|
||||||
|
from src.source import Git, Tarball
|
||||||
|
|
||||||
|
|
||||||
|
@contextlib.contextmanager
|
||||||
|
def cache_lock(layout: Layout):
|
||||||
|
layout.cache_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
f = open(layout.cache_lock, "w")
|
||||||
|
try:
|
||||||
|
fcntl.flock(f, fcntl.LOCK_EX)
|
||||||
|
yield
|
||||||
|
finally:
|
||||||
|
fcntl.flock(f, fcntl.LOCK_UN)
|
||||||
|
f.close()
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_tarball(layout: Layout, src: Tarball) -> Path:
|
||||||
|
dest = layout.tarball_cache / src.sha256
|
||||||
|
if dest.is_file():
|
||||||
|
return dest
|
||||||
|
if src.sha256 == "?":
|
||||||
|
log.warn(f"fetching {src.url} (sha256 unknown)")
|
||||||
|
else:
|
||||||
|
log.info(f"fetching {src.url}")
|
||||||
|
layout.tarball_cache.mkdir(parents=True, exist_ok=True)
|
||||||
|
tmp_fd, tmp_path = tempfile.mkstemp(dir=layout.tarball_cache, prefix=".tmp-")
|
||||||
|
tmp = Path(tmp_path)
|
||||||
|
h = hashlib.sha256()
|
||||||
|
try:
|
||||||
|
with os.fdopen(tmp_fd, "wb") as out, urllib.request.urlopen(src.url) as resp:
|
||||||
|
while True:
|
||||||
|
chunk = resp.read(1 << 20)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
out.write(chunk)
|
||||||
|
h.update(chunk)
|
||||||
|
got = h.hexdigest()
|
||||||
|
if src.sha256 == "?":
|
||||||
|
log.warn(f"computed sha256 = {got} (paste into recipe)")
|
||||||
|
final = layout.tarball_cache / got
|
||||||
|
os.replace(tmp, final)
|
||||||
|
return final
|
||||||
|
if got != src.sha256:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"sha256 mismatch for {src.url}: expected {src.sha256}, got {got}"
|
||||||
|
)
|
||||||
|
os.replace(tmp, dest)
|
||||||
|
return dest
|
||||||
|
except BaseException:
|
||||||
|
with contextlib.suppress(FileNotFoundError):
|
||||||
|
tmp.unlink()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_git(layout: Layout, src: Git) -> Path:
|
||||||
|
dest = layout.git_cache / src.commit
|
||||||
|
if dest.is_dir():
|
||||||
|
return dest
|
||||||
|
log.info(f"git clone {src.url} (commit {src.commit[:12]})")
|
||||||
|
layout.git_cache.mkdir(parents=True, exist_ok=True)
|
||||||
|
with tempfile.TemporaryDirectory(dir=layout.git_cache, prefix=".tmp-") as td:
|
||||||
|
bare = Path(td) / "bare"
|
||||||
|
subprocess.run(
|
||||||
|
["git", "clone", "--bare", "--quiet", src.url, str(bare)], check=True
|
||||||
|
)
|
||||||
|
r = subprocess.run(["git", "-C", str(bare), "cat-file", "-e", src.commit])
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError(f"commit {src.commit} not found in {src.url}")
|
||||||
|
os.replace(bare, dest)
|
||||||
|
return dest
|
||||||
|
|
||||||
|
|
||||||
|
def fetch(layout: Layout, src) -> Path:
|
||||||
|
with cache_lock(layout):
|
||||||
|
if isinstance(src, Tarball):
|
||||||
|
return fetch_tarball(layout, src)
|
||||||
|
if isinstance(src, Git):
|
||||||
|
return fetch_git(layout, src)
|
||||||
|
raise TypeError(f"unknown source type {type(src).__name__}")
|
||||||
|
|
||||||
|
|
||||||
|
def _extracted_marker(tree: Path) -> Path:
|
||||||
|
return tree.with_name(tree.name + ".extracted")
|
||||||
|
|
||||||
|
|
||||||
|
def _patched_marker(tree: Path) -> Path:
|
||||||
|
return tree.with_name(tree.name + ".patched")
|
||||||
|
|
||||||
|
|
||||||
|
def _wipe(tree: Path) -> None:
|
||||||
|
if tree.exists():
|
||||||
|
shutil.rmtree(tree)
|
||||||
|
for m in (_extracted_marker(tree), _patched_marker(tree)):
|
||||||
|
with contextlib.suppress(FileNotFoundError):
|
||||||
|
m.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def extract_tarball(cache_path: Path, tree: Path, strip: int) -> None:
|
||||||
|
tree.mkdir(parents=True, exist_ok=True)
|
||||||
|
# Use tar binary to support all formats including zstd
|
||||||
|
cmd = ["tar", "-xf", str(cache_path), "-C", str(tree)]
|
||||||
|
if strip:
|
||||||
|
cmd += [f"--strip-components={strip}"]
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_git(cache_path: Path, tree: Path, commit: str) -> None:
|
||||||
|
subprocess.run(["git", "clone", "--quiet", str(cache_path), str(tree)], check=True)
|
||||||
|
subprocess.run(
|
||||||
|
[
|
||||||
|
"git",
|
||||||
|
"-C",
|
||||||
|
str(tree),
|
||||||
|
"-c",
|
||||||
|
"advice.detachedHead=false",
|
||||||
|
"checkout",
|
||||||
|
"--quiet",
|
||||||
|
commit,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def extract(cache_path: Path, src, tree: Path) -> None:
|
||||||
|
_wipe(tree)
|
||||||
|
if isinstance(src, Tarball):
|
||||||
|
extract_tarball(cache_path, tree, src.strip_components)
|
||||||
|
elif isinstance(src, Git):
|
||||||
|
extract_git(cache_path, tree, src.commit)
|
||||||
|
else:
|
||||||
|
raise TypeError(f"unknown source type {type(src).__name__}")
|
||||||
|
_extracted_marker(tree).write_text("ok\n")
|
||||||
|
|
||||||
|
|
||||||
|
def apply_patches(tree: Path, recipe_dir: Path, patches: tuple[str, ...]) -> None:
|
||||||
|
if not patches:
|
||||||
|
_patched_marker(tree).write_text("\n")
|
||||||
|
return
|
||||||
|
pdir = recipe_dir / "patches"
|
||||||
|
for name in patches:
|
||||||
|
p = pdir / name
|
||||||
|
if not p.is_file():
|
||||||
|
raise FileNotFoundError(f"patch not found: {p}")
|
||||||
|
log.info(f" patch {name}")
|
||||||
|
with open(p, "rb") as fh:
|
||||||
|
r = subprocess.run(
|
||||||
|
["patch", "-p1", "--no-backup-if-mismatch", "--quiet"],
|
||||||
|
cwd=tree,
|
||||||
|
stdin=fh,
|
||||||
|
)
|
||||||
|
if r.returncode != 0:
|
||||||
|
raise RuntimeError(f"patch {name} failed in {tree}")
|
||||||
|
_patched_marker(tree).write_text("\n".join(patches) + "\n")
|
||||||
|
|
||||||
|
|
||||||
|
def prepare_source(layout: Layout, recipe_dir: Path, src, tree: Path) -> None:
|
||||||
|
"""Fetch + extract + patch into `tree`. Idempotent via marker files."""
|
||||||
|
cache_path = fetch(layout, src)
|
||||||
|
expected_patches = "\n".join(src.patches) + "\n" if src.patches else "\n"
|
||||||
|
if (
|
||||||
|
_patched_marker(tree).is_file()
|
||||||
|
and _patched_marker(tree).read_text() == expected_patches
|
||||||
|
):
|
||||||
|
return
|
||||||
|
log.info(f"extract {tree.name}")
|
||||||
|
extract(cache_path, src, tree)
|
||||||
|
apply_patches(tree, recipe_dir, src.patches)
|
||||||
-135
@@ -1,135 +0,0 @@
|
|||||||
use crate::recipe::{OutputPackage, PackageKind, RecipeSet, unresolved_deps};
|
|
||||||
use anyhow::{Result, anyhow, bail};
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PackageGraph {
|
|
||||||
outputs: BTreeMap<String, OutputPackage>,
|
|
||||||
target_edges: BTreeMap<String, Vec<String>>,
|
|
||||||
host_edges: BTreeMap<String, Vec<String>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PackageGraph {
|
|
||||||
pub fn new(recipes: &RecipeSet) -> Result<Self> {
|
|
||||||
let missing = unresolved_deps(recipes);
|
|
||||||
if !missing.is_empty() {
|
|
||||||
bail!("unresolved local dependencies:\n{}", missing.join("\n"));
|
|
||||||
}
|
|
||||||
let mut target_edges = BTreeMap::new();
|
|
||||||
let mut host_edges = BTreeMap::new();
|
|
||||||
for recipe in recipes.recipes.values() {
|
|
||||||
// host_deps always resolve into the host namespace.
|
|
||||||
let host_dep_keys: Vec<String> = recipe
|
|
||||||
.host_deps
|
|
||||||
.iter()
|
|
||||||
.map(|d| PackageKind::Host.key(d))
|
|
||||||
.collect();
|
|
||||||
for output in &recipe.outputs {
|
|
||||||
let key = output.key();
|
|
||||||
let mut edges = output.all_target_deps();
|
|
||||||
if output.name == recipe.name {
|
|
||||||
edges.extend(recipe.build_deps.iter().cloned());
|
|
||||||
edges.extend(recipe.deps.iter().cloned());
|
|
||||||
}
|
|
||||||
match output.kind {
|
|
||||||
PackageKind::Host => {
|
|
||||||
host_edges.insert(key, host_dep_keys.clone());
|
|
||||||
}
|
|
||||||
PackageKind::Target => {
|
|
||||||
let mut deps = host_dep_keys.clone();
|
|
||||||
deps.extend(edges);
|
|
||||||
target_edges.insert(key, deps);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Self {
|
|
||||||
outputs: recipes.outputs.clone(),
|
|
||||||
target_edges,
|
|
||||||
host_edges,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn output(&self, package: &str) -> Result<&OutputPackage> {
|
|
||||||
self.outputs
|
|
||||||
.get(package)
|
|
||||||
.ok_or_else(|| anyhow!("unknown package `{package}`"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn outputs(&self) -> impl Iterator<Item = &str> {
|
|
||||||
self.outputs.keys().map(String::as_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_order(&self, package: &str) -> Result<Vec<String>> {
|
|
||||||
self.output(package)?;
|
|
||||||
let mut visiting = BTreeSet::new();
|
|
||||||
let mut visited = BTreeSet::new();
|
|
||||||
let mut order = Vec::new();
|
|
||||||
self.visit(package, &mut visiting, &mut visited, &mut order)?;
|
|
||||||
Ok(order)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Topologically-ordered list of every output in the graph (host + target).
|
|
||||||
pub fn build_order_all(&self) -> Result<Vec<String>> {
|
|
||||||
let mut visiting = BTreeSet::new();
|
|
||||||
let mut visited = BTreeSet::new();
|
|
||||||
let mut order = Vec::new();
|
|
||||||
for package in self.outputs.keys() {
|
|
||||||
self.visit(package, &mut visiting, &mut visited, &mut order)?;
|
|
||||||
}
|
|
||||||
Ok(order)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit(
|
|
||||||
&self,
|
|
||||||
package: &str,
|
|
||||||
visiting: &mut BTreeSet<String>,
|
|
||||||
visited: &mut BTreeSet<String>,
|
|
||||||
order: &mut Vec<String>,
|
|
||||||
) -> Result<()> {
|
|
||||||
if visited.contains(package) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if !visiting.insert(package.to_owned()) {
|
|
||||||
bail!("dependency cycle involving `{package}`");
|
|
||||||
}
|
|
||||||
let deps = self
|
|
||||||
.target_edges
|
|
||||||
.get(package)
|
|
||||||
.or_else(|| self.host_edges.get(package))
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default();
|
|
||||||
for dep in deps {
|
|
||||||
self.visit(&dep, visiting, visited, order)?;
|
|
||||||
}
|
|
||||||
visiting.remove(package);
|
|
||||||
visited.insert(package.to_owned());
|
|
||||||
order.push(package.to_owned());
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn render(&self, package: Option<&str>) -> Result<Vec<String>> {
|
|
||||||
match package {
|
|
||||||
Some(package) => {
|
|
||||||
let order = self.build_order(package)?;
|
|
||||||
Ok(order
|
|
||||||
.into_iter()
|
|
||||||
.map(|pkg| format!("{pkg}: {:?}", self.edges(&pkg)))
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
None => Ok(self
|
|
||||||
.outputs
|
|
||||||
.keys()
|
|
||||||
.map(|pkg| format!("{pkg}: {:?}", self.edges(pkg)))
|
|
||||||
.collect()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn edges(&self, package: &str) -> Vec<String> {
|
|
||||||
self.target_edges
|
|
||||||
.get(package)
|
|
||||||
.or_else(|| self.host_edges.get(package))
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+107
@@ -0,0 +1,107 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Layout:
|
||||||
|
"""All filesystem paths used by the builder."""
|
||||||
|
|
||||||
|
repo: Path
|
||||||
|
build: Path
|
||||||
|
|
||||||
|
@property
|
||||||
|
def recipes_dir(self) -> Path:
|
||||||
|
return self.repo / "recipes"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_recipes_dir(self) -> Path:
|
||||||
|
return self.repo / "host-recipes"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def profiles_dir(self) -> Path:
|
||||||
|
return self.repo / "profiles"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def dockerfile(self) -> Path:
|
||||||
|
return self.repo / "Dockerfile"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cache_dir(self) -> Path:
|
||||||
|
return self.repo / "cache"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def tarball_cache(self) -> Path:
|
||||||
|
return self.cache_dir / "tarballs"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def git_cache(self) -> Path:
|
||||||
|
return self.cache_dir / "git"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cache_lock(self) -> Path:
|
||||||
|
return self.cache_dir / ".lock"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def sources_dir(self) -> Path:
|
||||||
|
return self.repo / "sources"
|
||||||
|
|
||||||
|
def source_tree(self, name: str, version: str) -> Path:
|
||||||
|
return self.sources_dir / f"{name}-{version}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def profile_link(self) -> Path:
|
||||||
|
return self.build / "profile"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def build_workdirs(self) -> Path:
|
||||||
|
return self.build / "builds"
|
||||||
|
|
||||||
|
def build_workdir(self, name: str) -> Path:
|
||||||
|
return self.build_workdirs / name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def host_pkgs_dir(self) -> Path:
|
||||||
|
return self.build / "host-pkgs"
|
||||||
|
|
||||||
|
def host_pkg_dir(self, name: str) -> Path:
|
||||||
|
return self.host_pkgs_dir / name
|
||||||
|
|
||||||
|
def host_pkg_marker(self, name: str, version: str, revision: int) -> Path:
|
||||||
|
return self.host_pkg_dir(name) / f".built-{version}-r{revision}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def pkgs_dir(self) -> Path:
|
||||||
|
return self.build / "pkgs"
|
||||||
|
|
||||||
|
def apk_path(self, output: str, version: str, revision: int) -> Path:
|
||||||
|
return self.pkgs_dir / f"{output}-{version}-r{revision}.apk"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def apkindex(self) -> Path:
|
||||||
|
return self.pkgs_dir / "Packages.adb"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def image_hash_file(self) -> Path:
|
||||||
|
return self.build / ".image-hash"
|
||||||
|
|
||||||
|
def ensure(self) -> None:
|
||||||
|
for p in (
|
||||||
|
self.cache_dir,
|
||||||
|
self.tarball_cache,
|
||||||
|
self.git_cache,
|
||||||
|
self.sources_dir,
|
||||||
|
self.build_workdirs,
|
||||||
|
self.host_pkgs_dir,
|
||||||
|
self.pkgs_dir,
|
||||||
|
):
|
||||||
|
p.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
def find_repo_root(start: Path) -> Path:
|
||||||
|
cur = start.resolve()
|
||||||
|
for p in (cur, *cur.parents):
|
||||||
|
if (p / "Dockerfile").is_file() and (p / "profiles").is_dir():
|
||||||
|
return p
|
||||||
|
raise FileNotFoundError(
|
||||||
|
f"Could not find Orchid repo root (looking for Dockerfile + profiles/) from {start}"
|
||||||
|
)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
def autotools_configure(self, extra_args=(), extra_env=None):
|
||||||
|
p = self.profile
|
||||||
|
env = {
|
||||||
|
"CFLAGS": p.get("cflags", ""),
|
||||||
|
"CXXFLAGS": p.get("cxxflags", ""),
|
||||||
|
"LDFLAGS": p.get("ldflags", ""),
|
||||||
|
}
|
||||||
|
if extra_env:
|
||||||
|
env.update(extra_env)
|
||||||
|
args = [
|
||||||
|
self.source_dir / "configure",
|
||||||
|
f"--host={p['triple']}",
|
||||||
|
f"--with-sysroot={self.sysroot}",
|
||||||
|
f"--prefix={p.get('prefix', '/usr')}",
|
||||||
|
f"--sysconfdir={p.get('sysconfdir', '/etc')}",
|
||||||
|
f"--localstatedir={p.get('localstatedir', '/var')}",
|
||||||
|
f"--bindir={p.get('bindir', '/usr/bin')}",
|
||||||
|
f"--sbindir={p.get('sbindir', '/usr/bin')}",
|
||||||
|
f"--libdir={p.get('libdir', '/usr/lib')}",
|
||||||
|
"--disable-static",
|
||||||
|
"--enable-shared",
|
||||||
|
*extra_args,
|
||||||
|
]
|
||||||
|
self.run(*args, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def autotools_build(self, extra_args=()):
|
||||||
|
self.run("make", f"-j{self.jobs}", *extra_args)
|
||||||
|
|
||||||
|
|
||||||
|
def autotools_install(self, extra_args=()):
|
||||||
|
self.run("make", "install", *extra_args, env={"DESTDIR": str(self.dest_dir)})
|
||||||
|
|
||||||
|
|
||||||
|
def autotools(*, configure_args=(), configure_env=None, build_args=(), install_args=()):
|
||||||
|
def _configure(self):
|
||||||
|
autotools_configure(self, configure_args, configure_env)
|
||||||
|
|
||||||
|
def _build(self):
|
||||||
|
autotools_build(self, build_args)
|
||||||
|
|
||||||
|
def _install(self):
|
||||||
|
autotools_install(self, (*install_args, f"DESTDIR={self.dest_dir}"))
|
||||||
|
|
||||||
|
return _configure, _build, _install
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
def cmake_configure(self, extra_args=(), extra_env=None, *, host=False):
|
||||||
|
p = self.profile
|
||||||
|
if host:
|
||||||
|
env = {
|
||||||
|
"CFLAGS": p.get("host_cflags", ""),
|
||||||
|
"CXXFLAGS": p.get("host_cxxflags", ""),
|
||||||
|
"LDFLAGS": p.get("host_ldflags", ""),
|
||||||
|
}
|
||||||
|
toolchain = []
|
||||||
|
else:
|
||||||
|
env = {
|
||||||
|
"CFLAGS": p.get("cflags", ""),
|
||||||
|
"CXXFLAGS": p.get("cxxflags", ""),
|
||||||
|
"LDFLAGS": p.get("ldflags", ""),
|
||||||
|
}
|
||||||
|
toolchain = [
|
||||||
|
"-DCMAKE_SYSTEM_NAME=Linux",
|
||||||
|
f"-DCMAKE_SYSTEM_PROCESSOR={p['arch']}",
|
||||||
|
f"-DCMAKE_SYSROOT={self.sysroot}",
|
||||||
|
f"-DCMAKE_C_COMPILER={p['triple']}-gcc",
|
||||||
|
f"-DCMAKE_CXX_COMPILER={p['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",
|
||||||
|
]
|
||||||
|
if extra_env:
|
||||||
|
env.update(extra_env)
|
||||||
|
self.run(
|
||||||
|
"cmake",
|
||||||
|
"-S",
|
||||||
|
self.source_dir,
|
||||||
|
"-B",
|
||||||
|
self.build_dir,
|
||||||
|
"-G",
|
||||||
|
"Ninja",
|
||||||
|
"-DCMAKE_BUILD_TYPE=Release",
|
||||||
|
f"-DCMAKE_INSTALL_PREFIX={p.get('prefix', '/usr')}",
|
||||||
|
f"-DCMAKE_INSTALL_SYSCONFDIR={p.get('sysconfdir', '/etc')}",
|
||||||
|
f"-DCMAKE_INSTALL_LOCALSTATEDIR={p.get('localstatedir', '/var')}",
|
||||||
|
*toolchain,
|
||||||
|
*extra_args,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cmake_build(self, extra_args=()):
|
||||||
|
self.run("cmake", "--build", self.build_dir, "-j", self.jobs, *extra_args)
|
||||||
|
|
||||||
|
|
||||||
|
def cmake_install(self, extra_args=()):
|
||||||
|
self.run(
|
||||||
|
"cmake",
|
||||||
|
"--install",
|
||||||
|
self.build_dir,
|
||||||
|
*extra_args,
|
||||||
|
env={"DESTDIR": str(self.dest_dir)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def cmake(
|
||||||
|
*, configure_args=(), configure_env=None, build_args=(), install_args=(), host=False
|
||||||
|
):
|
||||||
|
def _configure(self):
|
||||||
|
cmake_configure(self, configure_args, configure_env, host=host)
|
||||||
|
|
||||||
|
def _build(self):
|
||||||
|
cmake_build(self, build_args)
|
||||||
|
|
||||||
|
def _install(self):
|
||||||
|
cmake_install(self, install_args)
|
||||||
|
|
||||||
|
return _configure, _build, _install
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
def make_build(self, extra_args=(), *, env=None):
|
||||||
|
self.run("make", f"-j{self.jobs}", *extra_args, env=env)
|
||||||
|
|
||||||
|
|
||||||
|
def make_install(self, extra_args=(), *, env=None):
|
||||||
|
merged = {"DESTDIR": str(self.dest_dir)}
|
||||||
|
if env:
|
||||||
|
merged.update(env)
|
||||||
|
self.run("make", "install", *extra_args, env=merged)
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
def meson_cross_file(self):
|
||||||
|
cross = self.build_dir / "meson-cross.ini"
|
||||||
|
self.write_text(
|
||||||
|
cross,
|
||||||
|
f"""\
|
||||||
|
[binaries]
|
||||||
|
c = '{self.triple}-gcc'
|
||||||
|
cpp = '{self.triple}-g++'
|
||||||
|
ar = '{self.triple}-gcc-ar'
|
||||||
|
nm = '{self.triple}-nm'
|
||||||
|
objcopy = '{self.triple}-objcopy'
|
||||||
|
ranlib = '{self.triple}-ranlib'
|
||||||
|
strip = '{self.triple}-strip'
|
||||||
|
pkg-config = '{self.triple}-pkg-config'
|
||||||
|
|
||||||
|
[host_machine]
|
||||||
|
system = 'linux'
|
||||||
|
cpu_family = '{self.arch}'
|
||||||
|
cpu = '{self.arch}'
|
||||||
|
endian = 'little'
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
return cross
|
||||||
|
|
||||||
|
|
||||||
|
def meson_configure(
|
||||||
|
self, extra_args=(), extra_env=None, *, source_dir=None, flags=True, host=False
|
||||||
|
):
|
||||||
|
p = self.options
|
||||||
|
env = {}
|
||||||
|
if flags:
|
||||||
|
env.update(
|
||||||
|
{
|
||||||
|
"CFLAGS": p.get("cflags", ""),
|
||||||
|
"CXXFLAGS": p.get("cxxflags", ""),
|
||||||
|
"LDFLAGS": p.get("ldflags", ""),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if extra_env:
|
||||||
|
env.update(extra_env)
|
||||||
|
cross_args = [] if host else ["--cross-file", meson_cross_file(self)]
|
||||||
|
self.run(
|
||||||
|
"meson",
|
||||||
|
"setup",
|
||||||
|
source_dir or self.source_dir,
|
||||||
|
*cross_args,
|
||||||
|
f"--prefix={p.get('prefix', '/usr')}",
|
||||||
|
f"--sysconfdir={p.get('sysconfdir', '/etc')}",
|
||||||
|
f"--localstatedir={p.get('localstatedir', '/var')}",
|
||||||
|
"--libdir=lib",
|
||||||
|
"--sbindir=bin",
|
||||||
|
"--bindir=bin",
|
||||||
|
"--datadir=share",
|
||||||
|
"--buildtype=release",
|
||||||
|
"-Ddefault_library=shared",
|
||||||
|
*extra_args,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def meson_build(self, extra_args=()):
|
||||||
|
self.run("meson", "compile", f"-j{self.jobs}", *extra_args)
|
||||||
|
|
||||||
|
|
||||||
|
def meson_install(self, extra_args=()):
|
||||||
|
self.run(
|
||||||
|
"meson",
|
||||||
|
"install",
|
||||||
|
"--no-rebuild",
|
||||||
|
*extra_args,
|
||||||
|
env={"DESTDIR": str(self.dest_dir)},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def meson(*, configure_args=(), configure_env=None, build_args=(), install_args=()):
|
||||||
|
def _configure(self):
|
||||||
|
meson_configure(self, configure_args, configure_env)
|
||||||
|
|
||||||
|
def _build(self):
|
||||||
|
meson_build(self, build_args)
|
||||||
|
|
||||||
|
def _install(self):
|
||||||
|
meson_install(self, install_args)
|
||||||
|
|
||||||
|
return _configure, _build, _install
|
||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from typing import TextIO
|
||||||
|
|
||||||
|
_COLORS = {
|
||||||
|
"debug": "\033[2m",
|
||||||
|
"info": "\033[34m",
|
||||||
|
"warn": "\033[33m",
|
||||||
|
"error": "\033[31m",
|
||||||
|
"ok": "\033[32m",
|
||||||
|
}
|
||||||
|
_RESET = "\033[0m"
|
||||||
|
_BOLD = "\033[1m"
|
||||||
|
_TAGS = {"debug": "..", "info": "->", "warn": "!!", "error": "xx", "ok": "**"}
|
||||||
|
|
||||||
|
|
||||||
|
def _enabled(stream: TextIO | None = None) -> bool:
|
||||||
|
stream = stream or sys.stderr
|
||||||
|
return stream.isatty() and os.environ.get("NO_COLOR") is None
|
||||||
|
|
||||||
|
|
||||||
|
def tag(level: str, *, stream: TextIO | None = None) -> str:
|
||||||
|
tag_text = _TAGS[level]
|
||||||
|
if _enabled(stream):
|
||||||
|
return f"{_COLORS[level]}{tag_text}{_RESET}"
|
||||||
|
return tag_text
|
||||||
|
|
||||||
|
|
||||||
|
def bold(text: str, *, stream: TextIO | None = None) -> str:
|
||||||
|
if _enabled(stream):
|
||||||
|
return f"{_BOLD}{text}{_RESET}"
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
|
def _emit(level: str, msg: str) -> None:
|
||||||
|
sys.stderr.write(f"{tag(level)} {msg}\n")
|
||||||
|
sys.stderr.flush()
|
||||||
|
|
||||||
|
|
||||||
|
def field(label: str, value: str, *, stream: TextIO | None = None) -> str:
|
||||||
|
return f"{bold(label, stream=stream)}: {value}"
|
||||||
|
|
||||||
|
|
||||||
|
def debug(msg: str) -> None:
|
||||||
|
if os.environ.get("ORCHID_DEBUG"):
|
||||||
|
_emit("debug", msg)
|
||||||
|
|
||||||
|
|
||||||
|
def info(msg: str) -> None:
|
||||||
|
_emit("info", msg)
|
||||||
|
|
||||||
|
|
||||||
|
def info_field(label: str, value: str) -> None:
|
||||||
|
_emit("info", field(label, value))
|
||||||
|
|
||||||
|
|
||||||
|
def warn(msg: str) -> None:
|
||||||
|
_emit("warn", msg)
|
||||||
|
|
||||||
|
|
||||||
|
def error(msg: str) -> None:
|
||||||
|
_emit("error", msg)
|
||||||
|
|
||||||
|
|
||||||
|
def ok(msg: str) -> None:
|
||||||
|
_emit("ok", msg)
|
||||||
|
|
||||||
|
|
||||||
|
def ok_field(label: str, value: str) -> None:
|
||||||
|
_emit("ok", field(label, value))
|
||||||
-35
@@ -1,35 +0,0 @@
|
|||||||
//! Tiny stderr logger. We use a consistent `==> <action>: <details>` prefix
|
|
||||||
//! so progress messages are easy to scan during long builds.
|
|
||||||
|
|
||||||
use std::io::{IsTerminal, Write};
|
|
||||||
|
|
||||||
const ARROW: &str = "==>";
|
|
||||||
|
|
||||||
fn paint(color: &str, text: &str) -> String {
|
|
||||||
if std::io::stderr().is_terminal() {
|
|
||||||
format!("\x1b[{color}m{text}\x1b[0m")
|
|
||||||
} else {
|
|
||||||
text.to_owned()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit(color: &str, action: &str, details: &str) {
|
|
||||||
let arrow = paint(color, ARROW);
|
|
||||||
let action = paint("1", action);
|
|
||||||
let _ = writeln!(std::io::stderr(), "{arrow} {action} {details}");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Major step, e.g. starting a build or packaging an output.
|
|
||||||
pub fn step(action: &str, details: &str) {
|
|
||||||
emit("1;34", action, details); // bold blue
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Cache hit / skipped work.
|
|
||||||
pub fn skip(action: &str, details: &str) {
|
|
||||||
emit("1;33", action, details); // bold yellow
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sub-step inside a larger action.
|
|
||||||
pub fn info(action: &str, details: &str) {
|
|
||||||
emit("1;32", action, details); // bold green
|
|
||||||
}
|
|
||||||
-21
@@ -1,21 +0,0 @@
|
|||||||
mod apk;
|
|
||||||
mod build;
|
|
||||||
mod cli;
|
|
||||||
mod config;
|
|
||||||
mod graph;
|
|
||||||
mod log;
|
|
||||||
mod patches;
|
|
||||||
mod phase;
|
|
||||||
mod recipe;
|
|
||||||
mod rewrite;
|
|
||||||
mod source;
|
|
||||||
mod starlark;
|
|
||||||
mod update;
|
|
||||||
|
|
||||||
use anyhow::Result;
|
|
||||||
use clap::Parser;
|
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
|
||||||
let cli = cli::Cli::parse();
|
|
||||||
cli::run(cli)
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
pub fn discover(recipe_dir: &Path) -> Result<Vec<PathBuf>> {
|
|
||||||
let patch_dir = recipe_dir.join("patches");
|
|
||||||
if !patch_dir.exists() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
let mut patches = Vec::new();
|
|
||||||
for entry in std::fs::read_dir(&patch_dir)? {
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.extension().and_then(|ext| ext.to_str()) == Some("patch") {
|
|
||||||
patches.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patches.sort();
|
|
||||||
Ok(patches)
|
|
||||||
}
|
|
||||||
-213
@@ -1,213 +0,0 @@
|
|||||||
use crate::config::Config;
|
|
||||||
use crate::starlark::{eval_content_with_extra, prepend_common_lib_load};
|
|
||||||
use allocative::Allocative;
|
|
||||||
use anyhow::{Result, anyhow, bail};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use starlark::environment::{GlobalsBuilder, LibraryExtension};
|
|
||||||
use starlark::eval::Evaluator;
|
|
||||||
use starlark::starlark_module;
|
|
||||||
use starlark::values::none::NoneType;
|
|
||||||
use starlark::values::{ProvidesStaticType, Value};
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::collections::BTreeMap;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Allocative)]
|
|
||||||
pub struct PhaseCommand {
|
|
||||||
pub argv: Vec<String>,
|
|
||||||
/// Extra environment variables exported just for this command, in the
|
|
||||||
/// order the recipe supplied them. Empty means "inherit only".
|
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
||||||
pub env: Vec<(String, String)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// How `ctx.source_dir` is exposed to Starlark.
|
|
||||||
///
|
|
||||||
/// * `Single` is a single container path (e.g. `/source`), used when the
|
|
||||||
/// recipe declared a `source = {...}` form.
|
|
||||||
/// * `Many` is a map of source name to container path, exposed as a
|
|
||||||
/// `struct(...)` (e.g. `ctx.source_dir.linux`), used when the recipe
|
|
||||||
/// declared `sources = {"linux": {...}, ...}`.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub enum SourceDir {
|
|
||||||
Single(String),
|
|
||||||
Many(BTreeMap<String, String>),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Per-phase paths exposed to Starlark as `ctx.source_dir`, `ctx.build_dir`,
|
|
||||||
/// `ctx.dest_dir`, `ctx.prefix`, `ctx.sysroot` (Jinx-inspired).
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct PhaseEnv<'a> {
|
|
||||||
pub source_dir: SourceDir,
|
|
||||||
pub build_dir: &'a str,
|
|
||||||
pub dest_dir: &'a str,
|
|
||||||
pub prefix: &'a str,
|
|
||||||
pub sysroot: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, ProvidesStaticType, Allocative)]
|
|
||||||
struct CommandStore {
|
|
||||||
commands: RefCell<Vec<PhaseCommand>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl CommandStore {
|
|
||||||
fn push(&self, command: PhaseCommand) {
|
|
||||||
self.commands.borrow_mut().push(command);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[starlark_module]
|
|
||||||
fn phase_globals(builder: &mut GlobalsBuilder) {
|
|
||||||
fn ctx_run<'v>(
|
|
||||||
argv: Value<'v>,
|
|
||||||
#[starlark(require = named, default = NoneType)] env: Value<'v>,
|
|
||||||
eval: &mut Evaluator,
|
|
||||||
) -> anyhow::Result<NoneType> {
|
|
||||||
let json = argv.to_json()?;
|
|
||||||
let values: Vec<String> = serde_json::from_str(&json)
|
|
||||||
.map_err(|err| anyhow!("ctx.run expects a list of strings: {err}"))?;
|
|
||||||
if values.is_empty() {
|
|
||||||
bail!("ctx.run argv cannot be empty");
|
|
||||||
}
|
|
||||||
let env_vars = parse_env(env)?;
|
|
||||||
store(eval)?.push(PhaseCommand {
|
|
||||||
argv: values,
|
|
||||||
env: env_vars,
|
|
||||||
});
|
|
||||||
Ok(NoneType)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ctx_install(
|
|
||||||
src: &str,
|
|
||||||
dst: &str,
|
|
||||||
#[starlark(require = named, default = "644")] mode: &str,
|
|
||||||
eval: &mut Evaluator,
|
|
||||||
) -> anyhow::Result<NoneType> {
|
|
||||||
store(eval)?.push(PhaseCommand {
|
|
||||||
argv: vec![
|
|
||||||
"install".to_owned(),
|
|
||||||
format!("-Dm{mode}"),
|
|
||||||
src.to_owned(),
|
|
||||||
dst.to_owned(),
|
|
||||||
],
|
|
||||||
env: Vec::new(),
|
|
||||||
});
|
|
||||||
Ok(NoneType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_env(value: Value<'_>) -> anyhow::Result<Vec<(String, String)>> {
|
|
||||||
if value.is_none() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
let json = value.to_json()?;
|
|
||||||
let map: serde_json::Map<String, serde_json::Value> = serde_json::from_str(&json)
|
|
||||||
.map_err(|err| anyhow!("ctx.run env must be a dict of string -> string: {err}"))?;
|
|
||||||
let mut out = Vec::with_capacity(map.len());
|
|
||||||
for (key, val) in map {
|
|
||||||
let serde_json::Value::String(val) = val else {
|
|
||||||
bail!("ctx.run env value for `{key}` must be a string");
|
|
||||||
};
|
|
||||||
if key.is_empty() || key.contains('=') {
|
|
||||||
bail!("ctx.run env key `{key}` is not a valid variable name");
|
|
||||||
}
|
|
||||||
out.push((key, val));
|
|
||||||
}
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn store<'a, 'b, 'c, 'd>(eval: &'a Evaluator<'b, 'c, 'd>) -> anyhow::Result<&'a CommandStore> {
|
|
||||||
eval.extra
|
|
||||||
.ok_or_else(|| anyhow!("ctx command used without command store"))?
|
|
||||||
.downcast_ref::<CommandStore>()
|
|
||||||
.ok_or_else(|| anyhow!("command store has the wrong type"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn collect_phase_commands(
|
|
||||||
recipe_path: &Path,
|
|
||||||
repo_root: &Path,
|
|
||||||
config: &Config,
|
|
||||||
phase: &str,
|
|
||||||
env: &PhaseEnv<'_>,
|
|
||||||
package: Option<(&str, &str)>,
|
|
||||||
) -> Result<Vec<PhaseCommand>> {
|
|
||||||
validate_identifier(phase)?;
|
|
||||||
let raw = std::fs::read_to_string(recipe_path)?;
|
|
||||||
// Auto-load helpers from `lib/common.star` so recipes never need an
|
|
||||||
// explicit `load()` for the canonical helpers.
|
|
||||||
let mut content = prepend_common_lib_load(Some(repo_root), Some(config), &raw)?;
|
|
||||||
let jobs = std::thread::available_parallelism()
|
|
||||||
.map(|j| j.get())
|
|
||||||
.unwrap_or(1);
|
|
||||||
let source_dir_expr = source_dir_literal(&env.source_dir)?;
|
|
||||||
let ctx_literal = format!(
|
|
||||||
"struct(run = ctx_run, install = ctx_install, jobs = {jobs}, \
|
|
||||||
source_dir = {sd}, build_dir = {bd}, dest_dir = {dd}, prefix = {pf}, sysroot = {sr})",
|
|
||||||
sd = source_dir_expr,
|
|
||||||
bd = serde_json::to_string(env.build_dir)?,
|
|
||||||
dd = serde_json::to_string(env.dest_dir)?,
|
|
||||||
pf = serde_json::to_string(env.prefix)?,
|
|
||||||
sr = serde_json::to_string(env.sysroot)?,
|
|
||||||
);
|
|
||||||
let call = match package {
|
|
||||||
Some((name, destdir)) => format!(
|
|
||||||
"\n__ctx = {ctx_literal}\n__pkg = struct(name = {n}, destdir = {d})\n{phase}(__ctx, __pkg)\n",
|
|
||||||
n = serde_json::to_string(name)?,
|
|
||||||
d = serde_json::to_string(destdir)?,
|
|
||||||
),
|
|
||||||
None => format!("\n__ctx = {ctx_literal}\n{phase}(__ctx)\n"),
|
|
||||||
};
|
|
||||||
content.push_str(&call);
|
|
||||||
let globals = GlobalsBuilder::extended_by(&[LibraryExtension::StructType])
|
|
||||||
.with(phase_globals)
|
|
||||||
.build();
|
|
||||||
let cmd_store = CommandStore::default();
|
|
||||||
eval_content_with_extra(
|
|
||||||
recipe_path,
|
|
||||||
content,
|
|
||||||
Some(config),
|
|
||||||
Some(repo_root),
|
|
||||||
globals,
|
|
||||||
Some(&cmd_store),
|
|
||||||
)?;
|
|
||||||
Ok(cmd_store.commands.into_inner())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_dir_literal(source_dir: &SourceDir) -> Result<String> {
|
|
||||||
match source_dir {
|
|
||||||
SourceDir::Single(path) => Ok(serde_json::to_string(path)?),
|
|
||||||
SourceDir::Many(map) => {
|
|
||||||
let mut fields = Vec::with_capacity(map.len());
|
|
||||||
for (name, path) in map {
|
|
||||||
if !is_valid_field_name(name) {
|
|
||||||
bail!("source name `{name}` is not a valid Starlark identifier");
|
|
||||||
}
|
|
||||||
fields.push(format!("{name} = {}", serde_json::to_string(path)?));
|
|
||||||
}
|
|
||||||
Ok(format!("struct({})", fields.join(", ")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid_field_name(name: &str) -> bool {
|
|
||||||
let mut chars = name.chars();
|
|
||||||
match chars.next() {
|
|
||||||
Some(c) if c == '_' || c.is_ascii_alphabetic() => {}
|
|
||||||
_ => return false,
|
|
||||||
}
|
|
||||||
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_identifier(name: &str) -> Result<()> {
|
|
||||||
let mut chars = name.chars();
|
|
||||||
let Some(first) = chars.next() else {
|
|
||||||
bail!("phase function name cannot be empty");
|
|
||||||
};
|
|
||||||
if !(first == '_' || first.is_ascii_alphabetic()) {
|
|
||||||
bail!("invalid phase function name `{name}`");
|
|
||||||
}
|
|
||||||
if chars.any(|ch| !(ch == '_' || ch.is_ascii_alphanumeric())) {
|
|
||||||
bail!("invalid phase function name `{name}`");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
+171
@@ -0,0 +1,171 @@
|
|||||||
|
import graphlib
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from src.layout import Layout
|
||||||
|
from src.recipe import Recipe, RecipeSet
|
||||||
|
|
||||||
|
BUILD_PHASES = ("prepare", "configure", "build")
|
||||||
|
PHASE_STAMPS = {
|
||||||
|
"prepare": "prepared",
|
||||||
|
"configure": "configured",
|
||||||
|
"build": "built",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _key(r: Recipe) -> str:
|
||||||
|
return r.key # "host:<name>" or "<name>"
|
||||||
|
|
||||||
|
|
||||||
|
def transitive_host_deps(rs: RecipeSet, r: Recipe) -> list[str]:
|
||||||
|
"""Returns transitive host_deps for `r`, in topological order (deepest first)."""
|
||||||
|
seen: dict[str, Recipe] = {}
|
||||||
|
order: list[str] = []
|
||||||
|
|
||||||
|
def visit(name: str) -> None:
|
||||||
|
if name in seen:
|
||||||
|
return
|
||||||
|
h = rs.host.get(name)
|
||||||
|
if h is None:
|
||||||
|
raise KeyError(f"unknown host dep: {name}")
|
||||||
|
seen[name] = h
|
||||||
|
for hh in h.host_deps:
|
||||||
|
visit(hh)
|
||||||
|
order.append(name)
|
||||||
|
|
||||||
|
for d in r.host_deps:
|
||||||
|
visit(d)
|
||||||
|
return order
|
||||||
|
|
||||||
|
|
||||||
|
def is_built(layout: Layout, r: Recipe) -> bool:
|
||||||
|
if r.kind == "host":
|
||||||
|
return layout.host_pkg_marker(r.name, r.version, r.revision).is_file()
|
||||||
|
for out in r.outputs:
|
||||||
|
if not layout.apk_path(out, r.version, r.revision).is_file():
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def stamp_dir(layout: Layout, r: Recipe) -> Path:
|
||||||
|
wd = layout.build_workdir(r.name if r.kind == "target" else f"host-{r.name}")
|
||||||
|
return wd / ".orchid-stamps"
|
||||||
|
|
||||||
|
|
||||||
|
def stamp_token(r: Recipe) -> str:
|
||||||
|
return f"{r.version}-r{r.revision}\n"
|
||||||
|
|
||||||
|
|
||||||
|
def stamp_valid(layout: Layout, r: Recipe, phase: str) -> bool:
|
||||||
|
name = PHASE_STAMPS.get(phase)
|
||||||
|
if name is None:
|
||||||
|
return False
|
||||||
|
p = stamp_dir(layout, r) / name
|
||||||
|
try:
|
||||||
|
return p.read_text() == stamp_token(r)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def planned_stages(
|
||||||
|
layout: Layout, r: Recipe, *, forced: bool = False
|
||||||
|
) -> tuple[str, ...]:
|
||||||
|
stages: list[str] = []
|
||||||
|
for phase in BUILD_PHASES:
|
||||||
|
if phase in r.phases and (forced or not stamp_valid(layout, r, phase)):
|
||||||
|
stages.append(phase)
|
||||||
|
if "install" in r.phases:
|
||||||
|
stages.append("install")
|
||||||
|
stages.append("package" if r.kind == "target" else "finalize")
|
||||||
|
return tuple(stages)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Plan:
|
||||||
|
order: list[str] # ordered list of recipe keys
|
||||||
|
recipes: dict[str, Recipe] # all referenced recipes
|
||||||
|
forced: set[str] # keys to rebuild even if built
|
||||||
|
stages: dict[str, tuple[str, ...]] # stages each planned recipe will run
|
||||||
|
|
||||||
|
def __iter__(self):
|
||||||
|
return iter(self.order)
|
||||||
|
|
||||||
|
|
||||||
|
def _collect_targets(rs: RecipeSet, requested: list[str] | None) -> list[Recipe]:
|
||||||
|
if not requested:
|
||||||
|
return [r for r in rs.target.values() if r.enabled] + [
|
||||||
|
r for r in rs.host.values() if r.enabled
|
||||||
|
]
|
||||||
|
out: list[Recipe] = []
|
||||||
|
for spec in requested:
|
||||||
|
r = rs.get(spec)
|
||||||
|
if not r.enabled:
|
||||||
|
raise ValueError(f"{spec}: disabled by build_if = False")
|
||||||
|
out.append(r)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def build_plan(
|
||||||
|
rs: RecipeSet, layout: Layout, requested: list[str] | None, *, rebuild: bool = False
|
||||||
|
) -> Plan:
|
||||||
|
seen: dict[str, Recipe] = {}
|
||||||
|
ts: graphlib.TopologicalSorter[str] = graphlib.TopologicalSorter()
|
||||||
|
forced: set[str] = set()
|
||||||
|
|
||||||
|
def add(r: Recipe) -> None:
|
||||||
|
k = _key(r)
|
||||||
|
if k in seen:
|
||||||
|
return
|
||||||
|
seen[k] = r
|
||||||
|
deps: list[str] = []
|
||||||
|
for h in r.host_deps:
|
||||||
|
hr = rs.host.get(h)
|
||||||
|
if hr is None:
|
||||||
|
raise KeyError(f"{r.name}: unknown host dep {h!r}")
|
||||||
|
if not hr.enabled:
|
||||||
|
raise ValueError(f"{r.name}: host dep {h!r} disabled by build_if")
|
||||||
|
add(hr)
|
||||||
|
deps.append(_key(hr))
|
||||||
|
if r.kind == "target":
|
||||||
|
for d in (*r.deps, *r.run_deps):
|
||||||
|
tr = rs.target.get(d)
|
||||||
|
if tr is None:
|
||||||
|
raise KeyError(f"{r.name}: unknown dep {d!r}")
|
||||||
|
if not tr.enabled:
|
||||||
|
raise ValueError(f"{r.name}: dep {d!r} disabled by build_if")
|
||||||
|
add(tr)
|
||||||
|
deps.append(_key(tr))
|
||||||
|
else:
|
||||||
|
# host recipes may declare target `deps` that need to land in /sysroot
|
||||||
|
for d in r.deps:
|
||||||
|
tr = rs.target.get(d)
|
||||||
|
if tr is None:
|
||||||
|
raise KeyError(f"host:{r.name}: unknown target dep {d!r}")
|
||||||
|
if not tr.enabled:
|
||||||
|
raise ValueError(f"host:{r.name}: dep {d!r} disabled by build_if")
|
||||||
|
add(tr)
|
||||||
|
deps.append(_key(tr))
|
||||||
|
ts.add(k, *deps)
|
||||||
|
|
||||||
|
requested_recipes = _collect_targets(rs, requested)
|
||||||
|
for r in requested_recipes:
|
||||||
|
add(r)
|
||||||
|
if rebuild:
|
||||||
|
forced.add(_key(r))
|
||||||
|
|
||||||
|
order = list(ts.static_order())
|
||||||
|
|
||||||
|
stages: dict[str, tuple[str, ...]] = {}
|
||||||
|
final_order: list[str] = []
|
||||||
|
for k in order:
|
||||||
|
r = seen[k]
|
||||||
|
if k in forced:
|
||||||
|
stages[k] = planned_stages(layout, r, forced=True)
|
||||||
|
final_order.append(k)
|
||||||
|
continue
|
||||||
|
if is_built(layout, r):
|
||||||
|
continue
|
||||||
|
stages[k] = planned_stages(layout, r)
|
||||||
|
final_order.append(k)
|
||||||
|
|
||||||
|
return Plan(order=final_order, recipes=seen, forced=forced, stages=stages)
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import importlib.util
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from src.layout import Layout
|
||||||
|
|
||||||
|
REQUIRED_KEYS = ("arch", "triple", "container_image")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module(path: Path, name: str):
|
||||||
|
spec = importlib.util.spec_from_file_location(name, path)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError(f"cannot load profile module {path}")
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def load_profile(layout: Layout) -> dict[str, Any]:
|
||||||
|
link = layout.profile_link
|
||||||
|
if not link.exists():
|
||||||
|
raise FileNotFoundError(f"{link} missing a profile")
|
||||||
|
target = link.resolve()
|
||||||
|
if not target.is_file():
|
||||||
|
raise FileNotFoundError(f"profile {target.name}: missing config.py")
|
||||||
|
mod = _load_module(target, f"orchid_profile_{target.name}")
|
||||||
|
if not hasattr(mod, "profile"):
|
||||||
|
raise AttributeError(f"profile {target.name}: config.py must define profile()")
|
||||||
|
data = mod.profile()
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise TypeError(f"profile {target.name}: profile() must return a dict")
|
||||||
|
missing = [k for k in REQUIRED_KEYS if k not in data]
|
||||||
|
if missing:
|
||||||
|
raise ValueError(f"profile {target.name}: missing keys {missing}")
|
||||||
|
data["__name__"] = target.name
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def init_build_dir(build: Path, repo: Path, profile_name: str) -> None:
|
||||||
|
profile_src = repo / "profiles" / (profile_name + ".py")
|
||||||
|
if not (profile_src).is_file():
|
||||||
|
raise FileNotFoundError(f"profile {profile_name!r} not found at {profile_src}")
|
||||||
|
build.mkdir(parents=True, exist_ok=False)
|
||||||
|
link = build / "profile"
|
||||||
|
# Use relative symlink so the build dir can be moved with the repo.
|
||||||
|
import os
|
||||||
|
|
||||||
|
rel = Path(os.path.relpath(profile_src, build))
|
||||||
|
link.symlink_to(rel)
|
||||||
+258
@@ -0,0 +1,258 @@
|
|||||||
|
import importlib
|
||||||
|
import importlib.util
|
||||||
|
import pkgutil
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from functools import cache
|
||||||
|
from pathlib import Path, PurePosixPath
|
||||||
|
from typing import Any, Callable
|
||||||
|
|
||||||
|
from src import source as src_mod
|
||||||
|
from src.layout import Layout
|
||||||
|
from src.source import Subpackage, Tarball, subpackage, tarball, git
|
||||||
|
|
||||||
|
PHASE_NAMES = ("prepare", "configure", "build", "install")
|
||||||
|
LIB_DIR = Path(__file__).with_name("lib")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Recipe:
|
||||||
|
name: str
|
||||||
|
kind: str # "target" | "host"
|
||||||
|
dir: Path # recipe directory or parent (for pure form)
|
||||||
|
pure: bool
|
||||||
|
version: str
|
||||||
|
revision: int
|
||||||
|
description: str
|
||||||
|
license: str
|
||||||
|
url: str
|
||||||
|
maintainer: str
|
||||||
|
sources: dict[str | None, Tarball | src_mod.Git] # key=None for single source
|
||||||
|
host_deps: tuple[str, ...]
|
||||||
|
deps: tuple[str, ...]
|
||||||
|
run_deps: tuple[str, ...]
|
||||||
|
subpackages: tuple[Subpackage, ...]
|
||||||
|
phases: dict[str, Callable[[Any], None]]
|
||||||
|
enabled: bool
|
||||||
|
|
||||||
|
@property
|
||||||
|
def key(self) -> str:
|
||||||
|
return f"host:{self.name}" if self.kind == "host" else self.name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def patches_dir(self) -> Path | None:
|
||||||
|
if self.pure:
|
||||||
|
return None
|
||||||
|
p = self.dir / "patches"
|
||||||
|
return p if p.is_dir() else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def files_dir(self) -> Path | None:
|
||||||
|
if self.pure:
|
||||||
|
return None
|
||||||
|
p = self.dir / "files"
|
||||||
|
return p if p.is_dir() else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def outputs(self) -> list[str]:
|
||||||
|
if self.kind == "host":
|
||||||
|
return [self.name]
|
||||||
|
return [self.name, *(s.name for s in self.subpackages)]
|
||||||
|
|
||||||
|
|
||||||
|
# Import everything in lib/ for recipes
|
||||||
|
@cache
|
||||||
|
def _lib_symbols() -> dict:
|
||||||
|
ns = {}
|
||||||
|
if not LIB_DIR.is_dir():
|
||||||
|
return ns
|
||||||
|
|
||||||
|
for info in sorted(pkgutil.iter_modules([str(LIB_DIR)]), key=lambda i: i.name):
|
||||||
|
if info.ispkg:
|
||||||
|
continue
|
||||||
|
mod = importlib.import_module(f"src.lib.{info.name}")
|
||||||
|
names = getattr(mod, "__all__", None)
|
||||||
|
if names is None:
|
||||||
|
names = [n for n in mod.__dict__ if not n.startswith("_")]
|
||||||
|
for n in names:
|
||||||
|
ns[n] = getattr(mod, n)
|
||||||
|
return ns
|
||||||
|
|
||||||
|
|
||||||
|
# Symbols which are injected into the recipe
|
||||||
|
def _builtins(profile: dict) -> dict:
|
||||||
|
return {
|
||||||
|
**_lib_symbols(),
|
||||||
|
"tarball": tarball,
|
||||||
|
"git": git,
|
||||||
|
"subpackage": subpackage,
|
||||||
|
"path": PurePosixPath,
|
||||||
|
"profile": profile,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _load_module(path: Path, ns: dict):
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
f"orchid_recipe_{path.stem}_{id(path)}", path
|
||||||
|
)
|
||||||
|
if spec is None or spec.loader is None:
|
||||||
|
raise RuntimeError(f"cannot load {path}")
|
||||||
|
mod = importlib.util.module_from_spec(spec)
|
||||||
|
mod.__dict__.update(ns)
|
||||||
|
spec.loader.exec_module(mod)
|
||||||
|
return mod
|
||||||
|
|
||||||
|
|
||||||
|
def _plain_field(mod, recipe_name: str, field_name: str) -> str:
|
||||||
|
value = getattr(mod, field_name, "")
|
||||||
|
if value is None:
|
||||||
|
return ""
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError(f"{recipe_name}: '{field_name}' must be a string")
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _load_one(
|
||||||
|
name: str, kind: str, recipe_file: Path, recipe_dir: Path, pure: bool, profile: dict
|
||||||
|
) -> Recipe | None:
|
||||||
|
mod = _load_module(recipe_file, _builtins(profile))
|
||||||
|
|
||||||
|
version = getattr(mod, "version", None)
|
||||||
|
if not isinstance(version, str) or not version:
|
||||||
|
raise ValueError(f"{name}: 'version' (str) required")
|
||||||
|
revision = int(getattr(mod, "revision", 1))
|
||||||
|
if hasattr(mod, "metadata"):
|
||||||
|
raise TypeError(
|
||||||
|
f"{name}: use plain description/license/url/maintainer fields, not metadata = meta(...)"
|
||||||
|
)
|
||||||
|
description = _plain_field(mod, name, "description")
|
||||||
|
license = _plain_field(mod, name, "license")
|
||||||
|
url = _plain_field(mod, name, "url")
|
||||||
|
maintainer = _plain_field(mod, name, "maintainer")
|
||||||
|
|
||||||
|
single = getattr(mod, "source", None)
|
||||||
|
multi = getattr(mod, "sources", None)
|
||||||
|
if single is not None and multi is not None:
|
||||||
|
raise ValueError(f"{name}: define either 'source' or 'sources', not both")
|
||||||
|
sources = {}
|
||||||
|
if single is not None:
|
||||||
|
if not isinstance(single, (Tarball, src_mod.Git)):
|
||||||
|
raise TypeError(f"{name}: 'source' must be tarball()/git()")
|
||||||
|
sources[None] = single
|
||||||
|
elif multi is not None:
|
||||||
|
if not isinstance(multi, dict) or not multi:
|
||||||
|
raise TypeError(f"{name}: 'sources' must be a non-empty dict")
|
||||||
|
multi_items = list(multi.items())
|
||||||
|
for k, v in multi_items:
|
||||||
|
if not isinstance(k, str) or not k:
|
||||||
|
raise TypeError(f"{name}: 'sources' keys must be non-empty strings")
|
||||||
|
if not isinstance(v, (Tarball, src_mod.Git)):
|
||||||
|
raise TypeError(f"{name}: source {k!r} must be tarball()/git()")
|
||||||
|
if len(multi_items) == 1:
|
||||||
|
sources[None] = multi_items[0][1]
|
||||||
|
else:
|
||||||
|
for k, v in multi_items:
|
||||||
|
sources[k] = v
|
||||||
|
else:
|
||||||
|
raise ValueError(f"{name}: 'source' or 'sources' required")
|
||||||
|
|
||||||
|
if pure:
|
||||||
|
for s in sources.values():
|
||||||
|
if s.patches:
|
||||||
|
raise ValueError(
|
||||||
|
f"{name}: pure-form recipe cannot declare patches; convert to {name}/recipe.py + patches/"
|
||||||
|
)
|
||||||
|
|
||||||
|
host_deps = tuple(getattr(mod, "host_deps", ()) or ())
|
||||||
|
deps = tuple(getattr(mod, "deps", ()) or ())
|
||||||
|
run_deps = tuple(getattr(mod, "run_deps", ()) or ())
|
||||||
|
subs = tuple(getattr(mod, "subpackages", ()) or ())
|
||||||
|
for s in subs:
|
||||||
|
if not isinstance(s, Subpackage):
|
||||||
|
raise TypeError(
|
||||||
|
f"{name}: subpackages entries must be subpackage(...) values"
|
||||||
|
)
|
||||||
|
if kind == "host" and subs:
|
||||||
|
raise ValueError(f"{name}: host recipes do not support subpackages")
|
||||||
|
|
||||||
|
phases: dict[str, Callable] = {}
|
||||||
|
for pn in PHASE_NAMES:
|
||||||
|
fn = getattr(mod, pn, None)
|
||||||
|
if fn is not None:
|
||||||
|
if not callable(fn):
|
||||||
|
raise TypeError(f"{name}: '{pn}' must be a function")
|
||||||
|
phases[pn] = fn
|
||||||
|
if kind == "target" and "build" not in phases and "install" not in phases:
|
||||||
|
# Purely declarative pkgs are unusual but allowed
|
||||||
|
pass
|
||||||
|
|
||||||
|
enabled = bool(getattr(mod, "build_if", True))
|
||||||
|
|
||||||
|
return Recipe(
|
||||||
|
name=name,
|
||||||
|
kind=kind,
|
||||||
|
dir=recipe_dir,
|
||||||
|
pure=pure,
|
||||||
|
version=version,
|
||||||
|
revision=revision,
|
||||||
|
description=description,
|
||||||
|
license=license,
|
||||||
|
url=url,
|
||||||
|
maintainer=maintainer,
|
||||||
|
sources=sources,
|
||||||
|
host_deps=host_deps,
|
||||||
|
deps=deps,
|
||||||
|
run_deps=run_deps,
|
||||||
|
subpackages=subs,
|
||||||
|
phases=phases,
|
||||||
|
enabled=enabled,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _discover(root: Path, kind: str, profile: dict) -> dict[str, Recipe]:
|
||||||
|
out: dict[str, Recipe] = {}
|
||||||
|
if not root.is_dir():
|
||||||
|
return out
|
||||||
|
for entry in sorted(root.iterdir()):
|
||||||
|
if entry.name.startswith(".") or entry.name.startswith("_"):
|
||||||
|
continue
|
||||||
|
if entry.is_file() and entry.suffix == ".py":
|
||||||
|
name = entry.stem
|
||||||
|
r = _load_one(name, kind, entry, entry.parent, pure=True, profile=profile)
|
||||||
|
elif entry.is_dir():
|
||||||
|
rf = entry / "recipe.py"
|
||||||
|
if not rf.is_file():
|
||||||
|
continue
|
||||||
|
r = _load_one(entry.name, kind, rf, entry, pure=False, profile=profile)
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
if r is None:
|
||||||
|
continue
|
||||||
|
if r.name in out:
|
||||||
|
raise ValueError(f"duplicate {kind} recipe: {r.name}")
|
||||||
|
out[r.name] = r
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class RecipeSet:
|
||||||
|
target: dict[str, Recipe]
|
||||||
|
host: dict[str, Recipe]
|
||||||
|
|
||||||
|
def get(self, key: str) -> Recipe:
|
||||||
|
if key.startswith("host:"):
|
||||||
|
n = key[5:]
|
||||||
|
if n not in self.host:
|
||||||
|
raise KeyError(f"host recipe not found: {n}")
|
||||||
|
return self.host[n]
|
||||||
|
if key not in self.target:
|
||||||
|
raise KeyError(f"recipe not found: {key}")
|
||||||
|
return self.target[key]
|
||||||
|
|
||||||
|
def all(self) -> list[Recipe]:
|
||||||
|
return [*self.target.values(), *self.host.values()]
|
||||||
|
|
||||||
|
|
||||||
|
def load_recipes(layout: Layout, profile: dict) -> RecipeSet:
|
||||||
|
target = _discover(layout.recipes_dir, "target", profile)
|
||||||
|
host = _discover(layout.host_recipes_dir, "host", profile)
|
||||||
|
return RecipeSet(target=target, host=host)
|
||||||
-439
@@ -1,439 +0,0 @@
|
|||||||
use crate::config::Config;
|
|
||||||
use crate::starlark::{
|
|
||||||
eval_content, get_i32_default, get_json, get_string, get_string_default, get_string_vec,
|
|
||||||
has_name, prepend_common_lib_load,
|
|
||||||
};
|
|
||||||
use anyhow::{Result, anyhow, bail};
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub enum PackageKind {
|
|
||||||
Host,
|
|
||||||
Target,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PackageKind {
|
|
||||||
/// Canonical `host:`/bare key form used across the graph, CLI and
|
|
||||||
/// manifest layer. Host and target trees are completely separate
|
|
||||||
/// namespaces — they may share names.
|
|
||||||
pub fn key(&self, name: &str) -> String {
|
|
||||||
match self {
|
|
||||||
PackageKind::Host => format!("host:{name}"),
|
|
||||||
PackageKind::Target => name.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Filesystem-safe variant of [`PackageKind::key`] (no `:`), used to
|
|
||||||
/// derive build/source/manifest directory names.
|
|
||||||
pub fn slug(&self, name: &str) -> String {
|
|
||||||
match self {
|
|
||||||
PackageKind::Host => format!("host-{name}"),
|
|
||||||
PackageKind::Target => name.to_owned(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct Source {
|
|
||||||
/// Empty for the single-`source` form, otherwise the dict key from `sources`.
|
|
||||||
pub name: String,
|
|
||||||
pub url: String,
|
|
||||||
pub sha256: String,
|
|
||||||
/// Number of leading path components to strip when extracting (tar's
|
|
||||||
/// `--strip-components`). `0` means strip nothing.
|
|
||||||
pub strip_components: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
||||||
pub struct OutputPackage {
|
|
||||||
pub name: String,
|
|
||||||
/// Canonical key of the owning recipe (see [`PackageKind::key`]).
|
|
||||||
pub recipe: String,
|
|
||||||
pub kind: PackageKind,
|
|
||||||
pub version: String,
|
|
||||||
pub revision: i32,
|
|
||||||
pub description: String,
|
|
||||||
pub license: String,
|
|
||||||
/// Target packages installed into the build sysroot. Not propagated as
|
|
||||||
/// apk `depends:` metadata.
|
|
||||||
pub build_deps: Vec<String>,
|
|
||||||
/// Target packages declared as runtime dependencies (apk `depends:`).
|
|
||||||
/// Also installed into the sysroot so the recipe can link against them.
|
|
||||||
pub deps: Vec<String>,
|
|
||||||
pub install_fn: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutputPackage {
|
|
||||||
pub fn key(&self) -> String {
|
|
||||||
self.kind.key(&self.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Union of build- and run-dependencies (used to materialize the sysroot
|
|
||||||
/// and to compute the build graph).
|
|
||||||
pub fn all_target_deps(&self) -> Vec<String> {
|
|
||||||
let mut out = self.build_deps.clone();
|
|
||||||
out.extend(self.deps.iter().cloned());
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
||||||
pub struct Recipe {
|
|
||||||
pub id: String,
|
|
||||||
pub path: PathBuf,
|
|
||||||
pub dir: PathBuf,
|
|
||||||
pub name: String,
|
|
||||||
pub kind: PackageKind,
|
|
||||||
pub version: String,
|
|
||||||
pub revision: i32,
|
|
||||||
pub description: String,
|
|
||||||
pub license: String,
|
|
||||||
pub sources: Vec<Source>,
|
|
||||||
pub host_deps: Vec<String>,
|
|
||||||
pub build_deps: Vec<String>,
|
|
||||||
pub deps: Vec<String>,
|
|
||||||
pub outputs: Vec<OutputPackage>,
|
|
||||||
pub configure_fn: Option<String>,
|
|
||||||
pub build_fn: Option<String>,
|
|
||||||
pub check_fn: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Recipe {
|
|
||||||
/// Canonical key (`host:<id>` or `<id>`), used as the recipe-level
|
|
||||||
/// identifier in graphs, manifests and CLI references.
|
|
||||||
pub fn key(&self) -> String {
|
|
||||||
self.kind.key(&self.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Filesystem-safe variant of [`Recipe::key`].
|
|
||||||
pub fn slug(&self) -> String {
|
|
||||||
self.kind.slug(&self.id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct RecipeSet {
|
|
||||||
pub recipes: BTreeMap<String, Recipe>,
|
|
||||||
pub outputs: BTreeMap<String, OutputPackage>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecipeSet {
|
|
||||||
/// Discover recipes under `repo_root`:
|
|
||||||
/// * `recipes/<name>/recipe.star` → target packages
|
|
||||||
/// * `host-recipes/<name>/recipe.star` → host packages
|
|
||||||
pub fn load(repo_root: &Path, config: &Config) -> Result<Self> {
|
|
||||||
let mut recipes = BTreeMap::new();
|
|
||||||
for (subdir, kind) in [
|
|
||||||
("recipes", PackageKind::Target),
|
|
||||||
("host-recipes", PackageKind::Host),
|
|
||||||
] {
|
|
||||||
let root = repo_root.join(subdir);
|
|
||||||
if !root.exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for entry in WalkDir::new(&root).follow_links(false) {
|
|
||||||
let entry = entry?;
|
|
||||||
if entry.file_type().is_file() && entry.file_name() == "recipe.star" {
|
|
||||||
let recipe = Recipe::load(entry.path(), config, repo_root, kind.clone())?;
|
|
||||||
let key = recipe.key();
|
|
||||||
if recipes.insert(key.clone(), recipe).is_some() {
|
|
||||||
bail!("duplicate recipe `{key}` below {}", root.display());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut outputs = BTreeMap::new();
|
|
||||||
for recipe in recipes.values() {
|
|
||||||
for output in &recipe.outputs {
|
|
||||||
let key = output.key();
|
|
||||||
if outputs.insert(key.clone(), output.clone()).is_some() {
|
|
||||||
bail!("duplicate package output `{key}`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(Self { recipes, outputs })
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Look up a recipe by the package key produced by an output.
|
|
||||||
pub fn recipe_for_package(&self, package: &str) -> Result<&Recipe> {
|
|
||||||
let output = self
|
|
||||||
.outputs
|
|
||||||
.get(package)
|
|
||||||
.ok_or_else(|| anyhow!("unknown package `{package}`"))?;
|
|
||||||
self.recipes.get(&output.recipe).ok_or_else(|| {
|
|
||||||
anyhow!(
|
|
||||||
"package `{package}` references missing recipe `{}`",
|
|
||||||
output.recipe
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve a user-supplied reference (recipe key, output key, or bare
|
|
||||||
/// id — provided it isn't ambiguous between the host and target trees).
|
|
||||||
pub fn recipe_by_user_ref(&self, name: &str) -> Result<&Recipe> {
|
|
||||||
if let Some(recipe) = self.recipes.get(name) {
|
|
||||||
return Ok(recipe);
|
|
||||||
}
|
|
||||||
if self.outputs.contains_key(name) {
|
|
||||||
return self.recipe_for_package(name);
|
|
||||||
}
|
|
||||||
// Bare id: search both trees, error on ambiguity.
|
|
||||||
let host_key = PackageKind::Host.key(name);
|
|
||||||
let target_key = PackageKind::Target.key(name);
|
|
||||||
match (self.recipes.get(&host_key), self.recipes.get(&target_key)) {
|
|
||||||
(Some(_), Some(_)) => bail!(
|
|
||||||
"`{name}` is ambiguous: matches both `{host_key}` and `{target_key}`; \
|
|
||||||
use the explicit form"
|
|
||||||
),
|
|
||||||
(Some(r), None) | (None, Some(r)) => Ok(r),
|
|
||||||
(None, None) => bail!("unknown recipe `{name}`"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Recipe {
|
|
||||||
pub fn load(path: &Path, config: &Config, repo_root: &Path, kind: PackageKind) -> Result<Self> {
|
|
||||||
// Auto-load helpers from `lib/common.star` so recipes never need an
|
|
||||||
// explicit `load()` for the canonical helpers.
|
|
||||||
let raw = std::fs::read_to_string(path)?;
|
|
||||||
let content = prepend_common_lib_load(Some(repo_root), Some(config), &raw)?;
|
|
||||||
let module = eval_content(
|
|
||||||
path,
|
|
||||||
content,
|
|
||||||
Some(config),
|
|
||||||
Some(repo_root),
|
|
||||||
starlark::environment::Globals::standard(),
|
|
||||||
)?;
|
|
||||||
let dir = path
|
|
||||||
.parent()
|
|
||||||
.unwrap_or_else(|| Path::new("."))
|
|
||||||
.to_path_buf();
|
|
||||||
let id = dir
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.ok_or_else(|| anyhow!("recipe path has no package directory: {}", path.display()))?
|
|
||||||
.to_owned();
|
|
||||||
let name = get_string(&module, "name")?;
|
|
||||||
let version = get_string(&module, "version")?;
|
|
||||||
let revision = get_i32_default(&module, "revision", 0)?;
|
|
||||||
let description = get_string_default(&module, "description", "???")?;
|
|
||||||
let license = get_string_default(&module, "license", "???")?;
|
|
||||||
let build_deps = get_string_vec(&module, "build_deps")?;
|
|
||||||
let deps = get_string_vec(&module, "deps")?;
|
|
||||||
let host_deps = get_string_vec(&module, "host_deps")?;
|
|
||||||
let sources = parse_sources(get_json(&module, "sources")?, get_json(&module, "source")?)?;
|
|
||||||
let subpackages = parse_subpackages(get_json(&module, "subpackages")?)?;
|
|
||||||
let mut outputs = Vec::new();
|
|
||||||
let recipe_key = kind.key(&id);
|
|
||||||
outputs.push(OutputPackage {
|
|
||||||
name: name.clone(),
|
|
||||||
recipe: recipe_key.clone(),
|
|
||||||
kind: kind.clone(),
|
|
||||||
version: version.clone(),
|
|
||||||
revision,
|
|
||||||
description: description.clone(),
|
|
||||||
license: license.clone(),
|
|
||||||
build_deps: build_deps.clone(),
|
|
||||||
deps: deps.clone(),
|
|
||||||
install_fn: "install".to_owned(),
|
|
||||||
});
|
|
||||||
for subpkg in subpackages {
|
|
||||||
let sub_name = subpkg
|
|
||||||
.get("name")
|
|
||||||
.and_then(JsonValue::as_str)
|
|
||||||
.ok_or_else(|| anyhow!("subpackage in `{name}` is missing string `name`"))?
|
|
||||||
.to_owned();
|
|
||||||
outputs.push(OutputPackage {
|
|
||||||
name: sub_name,
|
|
||||||
recipe: recipe_key.clone(),
|
|
||||||
kind: kind.clone(),
|
|
||||||
version: version.clone(),
|
|
||||||
revision,
|
|
||||||
description: subpkg
|
|
||||||
.get("description")
|
|
||||||
.and_then(JsonValue::as_str)
|
|
||||||
.unwrap_or(&description)
|
|
||||||
.to_owned(),
|
|
||||||
license: subpkg
|
|
||||||
.get("license")
|
|
||||||
.and_then(JsonValue::as_str)
|
|
||||||
.unwrap_or(&license)
|
|
||||||
.to_owned(),
|
|
||||||
build_deps: json_string_list(subpkg.get("build_deps"), "subpackage build_deps")?
|
|
||||||
.unwrap_or_default(),
|
|
||||||
deps: json_string_list(subpkg.get("deps"), "subpackage deps")?.unwrap_or_default(),
|
|
||||||
install_fn: subpkg
|
|
||||||
.get("install")
|
|
||||||
.and_then(JsonValue::as_str)
|
|
||||||
.unwrap_or("install")
|
|
||||||
.to_owned(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
validate_required(&outputs)?;
|
|
||||||
Ok(Self {
|
|
||||||
id,
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
dir,
|
|
||||||
name,
|
|
||||||
kind,
|
|
||||||
version,
|
|
||||||
revision,
|
|
||||||
description,
|
|
||||||
license,
|
|
||||||
sources,
|
|
||||||
host_deps,
|
|
||||||
build_deps,
|
|
||||||
deps,
|
|
||||||
outputs,
|
|
||||||
configure_fn: has_name(&module, "configure").then_some("configure".to_owned()),
|
|
||||||
build_fn: has_name(&module, "build").then_some("build".to_owned()),
|
|
||||||
check_fn: has_name(&module, "check").then_some("check".to_owned()),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_required(outputs: &[OutputPackage]) -> Result<()> {
|
|
||||||
for output in outputs {
|
|
||||||
if output.name.trim().is_empty() {
|
|
||||||
bail!("package output name cannot be empty");
|
|
||||||
}
|
|
||||||
for (field, value) in [
|
|
||||||
("version", &output.version),
|
|
||||||
("description", &output.description),
|
|
||||||
("license", &output.license),
|
|
||||||
] {
|
|
||||||
if value.trim().is_empty() {
|
|
||||||
bail!(
|
|
||||||
"package `{}` has empty required field `{field}`",
|
|
||||||
output.name
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_sources(
|
|
||||||
sources: Option<JsonValue>,
|
|
||||||
legacy_source: Option<JsonValue>,
|
|
||||||
) -> Result<Vec<Source>> {
|
|
||||||
match (sources, legacy_source) {
|
|
||||||
(Some(_), Some(_)) => bail!("recipe defines both `sources` and `source`; use only one"),
|
|
||||||
(None, None) => Ok(Vec::new()),
|
|
||||||
(None, Some(single)) => {
|
|
||||||
let obj = single
|
|
||||||
.as_object()
|
|
||||||
.ok_or_else(|| anyhow!("`source` must be a dict"))?;
|
|
||||||
Ok(vec![parse_source_entry(String::new(), obj)?])
|
|
||||||
}
|
|
||||||
(Some(multi), None) => {
|
|
||||||
let obj = multi
|
|
||||||
.as_object()
|
|
||||||
.ok_or_else(|| anyhow!("`sources` must be a dict of {{name: source}}"))?;
|
|
||||||
obj.iter()
|
|
||||||
.map(|(name, value)| {
|
|
||||||
if name.is_empty() {
|
|
||||||
bail!("source name in `sources` cannot be empty");
|
|
||||||
}
|
|
||||||
let entry = value
|
|
||||||
.as_object()
|
|
||||||
.ok_or_else(|| anyhow!("source `{name}` must be a dict"))?;
|
|
||||||
parse_source_entry(name.clone(), entry)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_source_entry(name: String, obj: &serde_json::Map<String, JsonValue>) -> Result<Source> {
|
|
||||||
let url = obj
|
|
||||||
.get("url")
|
|
||||||
.and_then(JsonValue::as_str)
|
|
||||||
.ok_or_else(|| anyhow!("source entry missing string `url`"))?
|
|
||||||
.to_owned();
|
|
||||||
let sha256 = obj
|
|
||||||
.get("sha256")
|
|
||||||
.and_then(JsonValue::as_str)
|
|
||||||
.unwrap_or("???")
|
|
||||||
.to_owned();
|
|
||||||
let strip_components = match obj.get("strip_components") {
|
|
||||||
None => 0,
|
|
||||||
Some(JsonValue::Number(n)) => n
|
|
||||||
.as_u64()
|
|
||||||
.and_then(|v| u32::try_from(v).ok())
|
|
||||||
.ok_or_else(|| anyhow!("source `strip_components` must be a non-negative integer"))?,
|
|
||||||
Some(_) => bail!("source `strip_components` must be an integer"),
|
|
||||||
};
|
|
||||||
Ok(Source {
|
|
||||||
name,
|
|
||||||
url,
|
|
||||||
sha256,
|
|
||||||
strip_components,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_subpackages(value: Option<JsonValue>) -> Result<Vec<serde_json::Map<String, JsonValue>>> {
|
|
||||||
match value {
|
|
||||||
Some(JsonValue::Array(values)) => values
|
|
||||||
.into_iter()
|
|
||||||
.map(|value| {
|
|
||||||
value
|
|
||||||
.as_object()
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| anyhow!("subpackages entries must be objects"))
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
Some(_) => bail!("subpackages must be a list of objects"),
|
|
||||||
None => Ok(Vec::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn json_string_list(value: Option<&JsonValue>, label: &str) -> Result<Option<Vec<String>>> {
|
|
||||||
match value {
|
|
||||||
Some(JsonValue::Array(values)) => values
|
|
||||||
.iter()
|
|
||||||
.map(|value| {
|
|
||||||
value
|
|
||||||
.as_str()
|
|
||||||
.map(ToOwned::to_owned)
|
|
||||||
.ok_or_else(|| anyhow!("{label} must contain only strings"))
|
|
||||||
})
|
|
||||||
.collect::<Result<Vec<_>>>()
|
|
||||||
.map(Some),
|
|
||||||
Some(_) => bail!("{label} must be a string list"),
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn unresolved_deps(recipes: &RecipeSet) -> Vec<String> {
|
|
||||||
let names: BTreeSet<_> = recipes.outputs.keys().cloned().collect();
|
|
||||||
let mut missing = Vec::new();
|
|
||||||
for recipe in recipes.recipes.values() {
|
|
||||||
// host_deps always refer to host outputs (canonical `host:<name>`);
|
|
||||||
// build_deps / deps refer to target outputs (bare names).
|
|
||||||
for dep in &recipe.host_deps {
|
|
||||||
let key = PackageKind::Host.key(dep);
|
|
||||||
if !names.contains(&key) {
|
|
||||||
missing.push(format!("{} -> {key}", recipe.key()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for dep in recipe.build_deps.iter().chain(recipe.deps.iter()) {
|
|
||||||
if !names.contains(dep) {
|
|
||||||
missing.push(format!("{} -> {dep}", recipe.key()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for output in &recipe.outputs {
|
|
||||||
for dep in output.build_deps.iter().chain(output.deps.iter()) {
|
|
||||||
if !names.contains(dep) {
|
|
||||||
missing.push(format!("{} -> {dep}", output.key()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
missing
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
use anyhow::{Context, Result};
|
|
||||||
use std::fs;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
||||||
pub struct Rewrite {
|
|
||||||
pub field: String,
|
|
||||||
pub old: String,
|
|
||||||
pub new: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn backup_path(path: &Path) -> PathBuf {
|
|
||||||
let mut backup = path.as_os_str().to_os_string();
|
|
||||||
backup.push(".bak");
|
|
||||||
PathBuf::from(backup)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn rewrite_placeholders(path: &Path, rewrites: &[Rewrite]) -> Result<bool> {
|
|
||||||
if rewrites.is_empty() {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
let original =
|
|
||||||
fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
|
|
||||||
let mut updated = original.clone();
|
|
||||||
for rewrite in rewrites {
|
|
||||||
let quoted_old = format!("{} = \"{}\"", rewrite.field, rewrite.old);
|
|
||||||
let quoted_new = format!("{} = \"{}\"", rewrite.field, rewrite.new);
|
|
||||||
updated = updated.replacen("ed_old, "ed_new, 1);
|
|
||||||
let dict_old = format!("\"{}\": \"{}\"", rewrite.field, rewrite.old);
|
|
||||||
let dict_new = format!("\"{}\": \"{}\"", rewrite.field, rewrite.new);
|
|
||||||
updated = updated.replacen(&dict_old, &dict_new, 1);
|
|
||||||
}
|
|
||||||
if updated == original {
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
fs::write(backup_path(path), original)?;
|
|
||||||
fs::write(path, updated)?;
|
|
||||||
Ok(true)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Tarball:
|
||||||
|
url: str
|
||||||
|
sha256: str
|
||||||
|
strip_components: int = 1
|
||||||
|
patches: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cache_key(self) -> str:
|
||||||
|
return self.sha256
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Git:
|
||||||
|
url: str
|
||||||
|
commit: str
|
||||||
|
patches: tuple[str, ...] = ()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cache_key(self) -> str:
|
||||||
|
return self.commit
|
||||||
|
|
||||||
|
|
||||||
|
def tarball(
|
||||||
|
*,
|
||||||
|
url: str,
|
||||||
|
sha256: str,
|
||||||
|
strip_components: int = 1,
|
||||||
|
patches: list[str] | tuple[str, ...] = (),
|
||||||
|
) -> Tarball:
|
||||||
|
return Tarball(
|
||||||
|
url=url,
|
||||||
|
sha256=sha256,
|
||||||
|
strip_components=strip_components,
|
||||||
|
patches=tuple(patches),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def git(*, url: str, commit: str, patches: list[str] | tuple[str, ...] = ()) -> Git:
|
||||||
|
if not commit or commit == "?":
|
||||||
|
raise ValueError("git source requires an explicit commit SHA")
|
||||||
|
return Git(url=url, commit=commit, patches=tuple(patches))
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Subpackage:
|
||||||
|
name: str
|
||||||
|
files: tuple[str, ...]
|
||||||
|
description: str = ""
|
||||||
|
license: str = ""
|
||||||
|
url: str = ""
|
||||||
|
maintainer: str = ""
|
||||||
|
|
||||||
|
|
||||||
|
def subpackage(
|
||||||
|
name: str,
|
||||||
|
*,
|
||||||
|
description: str = "",
|
||||||
|
license: str = "",
|
||||||
|
url: str = "",
|
||||||
|
maintainer: str = "",
|
||||||
|
files: list[str] | tuple[str, ...] = (),
|
||||||
|
) -> Subpackage:
|
||||||
|
for field_name, value in (
|
||||||
|
("description", description),
|
||||||
|
("license", license),
|
||||||
|
("url", url),
|
||||||
|
("maintainer", maintainer),
|
||||||
|
):
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise TypeError(f"subpackage {name}: '{field_name}' must be a string")
|
||||||
|
for pat in files:
|
||||||
|
if pat.startswith("/") or ".." in pat.split("/"):
|
||||||
|
raise ValueError(
|
||||||
|
f"subpackage {name}: file pattern {pat!r} must be relative and free of '..'"
|
||||||
|
)
|
||||||
|
return Subpackage(
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
license=license,
|
||||||
|
url=url,
|
||||||
|
maintainer=maintainer,
|
||||||
|
files=tuple(files),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def patches_of(src) -> tuple[str, ...]:
|
||||||
|
return src.patches
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
use crate::log;
|
|
||||||
use crate::recipe::Recipe;
|
|
||||||
use crate::rewrite::{Rewrite, rewrite_placeholders};
|
|
||||||
use anyhow::{Context, Result, bail};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::fs;
|
|
||||||
use std::io::Read;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
pub fn fetch_sources(recipe: &Recipe, cache_dir: &Path) -> Result<Vec<PathBuf>> {
|
|
||||||
fs::create_dir_all(cache_dir)?;
|
|
||||||
let mut rewrites = Vec::new();
|
|
||||||
let mut paths = Vec::new();
|
|
||||||
for source in &recipe.sources {
|
|
||||||
let label = if source.name.is_empty() {
|
|
||||||
recipe.key()
|
|
||||||
} else {
|
|
||||||
format!("{}:{}", recipe.key(), source.name)
|
|
||||||
};
|
|
||||||
let cached = source.sha256 != "???" && cache_dir.join(&source.sha256).exists();
|
|
||||||
if cached {
|
|
||||||
log::skip("cached", &format!("{label} ({})", source.url));
|
|
||||||
paths.push(cache_dir.join(&source.sha256));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
log::step("fetch", &format!("{label} <- {}", source.url));
|
|
||||||
let bytes = download(&source.url)?;
|
|
||||||
let actual = sha256_hex(&bytes);
|
|
||||||
if source.sha256 == "???" {
|
|
||||||
log::info("sha256", &format!("{label} = {actual}"));
|
|
||||||
rewrites.push(Rewrite {
|
|
||||||
field: "sha256".into(),
|
|
||||||
old: "???".into(),
|
|
||||||
new: actual.clone(),
|
|
||||||
});
|
|
||||||
} else if source.sha256 != actual {
|
|
||||||
bail!(
|
|
||||||
"checksum mismatch for {}: expected {}, got {}",
|
|
||||||
source.url,
|
|
||||||
source.sha256,
|
|
||||||
actual
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let path = cache_dir.join(&actual);
|
|
||||||
if !path.exists() {
|
|
||||||
fs::write(&path, &bytes)
|
|
||||||
.with_context(|| format!("failed to write {}", path.display()))?;
|
|
||||||
}
|
|
||||||
paths.push(path);
|
|
||||||
}
|
|
||||||
rewrite_placeholders(&recipe.path, &rewrites)?;
|
|
||||||
Ok(paths)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn download(url: &str) -> Result<Vec<u8>> {
|
|
||||||
if let Some(path) = url.strip_prefix("file://") {
|
|
||||||
return Ok(fs::read(path)?);
|
|
||||||
}
|
|
||||||
// Some mirrors reject requests without a User-Agent with HTTP 403, so set an explicit one.
|
|
||||||
let client = reqwest::blocking::Client::builder()
|
|
||||||
.user_agent(concat!("distro/", env!("CARGO_PKG_VERSION")))
|
|
||||||
.build()?;
|
|
||||||
let mut response = client.get(url).send()?.error_for_status()?;
|
|
||||||
let mut bytes = Vec::new();
|
|
||||||
response.read_to_end(&mut bytes)?;
|
|
||||||
Ok(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sha256_hex(bytes: &[u8]) -> String {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(bytes);
|
|
||||||
hex::encode(hasher.finalize())
|
|
||||||
}
|
|
||||||
-513
@@ -1,513 +0,0 @@
|
|||||||
use crate::config::Config;
|
|
||||||
use allocative::{Allocative, Visitor, ident_key};
|
|
||||||
use anyhow::{Result, anyhow, bail};
|
|
||||||
use serde_json::Value as JsonValue;
|
|
||||||
use starlark::environment::{FrozenModule, Globals, Module};
|
|
||||||
use starlark::eval::{Evaluator, FileLoader};
|
|
||||||
use starlark::starlark_simple_value;
|
|
||||||
use starlark::syntax::{AstModule, Dialect};
|
|
||||||
use starlark::values::dict::AllocDict;
|
|
||||||
use starlark::values::{AnyLifetime, Heap, NoSerialize, ProvidesStaticType, StarlarkValue, Value};
|
|
||||||
use starlark_derive::starlark_value;
|
|
||||||
use std::collections::{BTreeMap, HashMap};
|
|
||||||
use std::fmt::{self, Display};
|
|
||||||
use std::mem;
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub struct OptionsValue {
|
|
||||||
values: BTreeMap<String, JsonValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OptionsValue {
|
|
||||||
fn new(values: BTreeMap<String, JsonValue>) -> Result<Self> {
|
|
||||||
for key in values.keys() {
|
|
||||||
validate_starlark_identifier(key)?;
|
|
||||||
}
|
|
||||||
Ok(Self { values })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for OptionsValue {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "options")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark_simple_value!(OptionsValue);
|
|
||||||
|
|
||||||
impl Allocative for OptionsValue {
|
|
||||||
fn visit<'a, 'b: 'a>(&self, visitor: &'a mut Visitor<'b>) {
|
|
||||||
let mut visitor = visitor.enter_self(self);
|
|
||||||
visitor.visit_simple(
|
|
||||||
ident_key!(values),
|
|
||||||
mem::size_of::<(String, JsonValue)>() * self.values.len(),
|
|
||||||
);
|
|
||||||
visitor.exit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[starlark_value(type = "options")]
|
|
||||||
impl<'v> StarlarkValue<'v> for OptionsValue {
|
|
||||||
fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option<Value<'v>> {
|
|
||||||
self.values.get(attr).map(|value| heap.alloc(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, ProvidesStaticType, NoSerialize, Allocative)]
|
|
||||||
pub struct SettingsValue {
|
|
||||||
target_arch: String,
|
|
||||||
container_runtime: String,
|
|
||||||
container_image: String,
|
|
||||||
container_dockerfile: String,
|
|
||||||
options: BTreeMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&Config> for SettingsValue {
|
|
||||||
fn from(config: &Config) -> Self {
|
|
||||||
Self {
|
|
||||||
target_arch: config.target_arch.clone(),
|
|
||||||
container_runtime: config.container_runtime.clone(),
|
|
||||||
container_image: config.container_image.clone(),
|
|
||||||
container_dockerfile: config.container_dockerfile.display().to_string(),
|
|
||||||
options: config
|
|
||||||
.options
|
|
||||||
.iter()
|
|
||||||
.map(|(key, value)| (key.clone(), option_value_to_string(value)))
|
|
||||||
.collect(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Display for SettingsValue {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
write!(f, "settings")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark_simple_value!(SettingsValue);
|
|
||||||
|
|
||||||
#[starlark_value(type = "settings")]
|
|
||||||
impl<'v> StarlarkValue<'v> for SettingsValue {
|
|
||||||
fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option<Value<'v>> {
|
|
||||||
match attr {
|
|
||||||
"target_arch" => Some(heap.alloc(self.target_arch.as_str())),
|
|
||||||
"container_runtime" => Some(heap.alloc(self.container_runtime.as_str())),
|
|
||||||
"container_image" => Some(heap.alloc(self.container_image.as_str())),
|
|
||||||
"container_dockerfile" => Some(heap.alloc(self.container_dockerfile.as_str())),
|
|
||||||
"options" => Some(heap.alloc(AllocDict(self.options.clone()))),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_file(
|
|
||||||
path: &Path,
|
|
||||||
settings: Option<&Config>,
|
|
||||||
repo_root: Option<&Path>,
|
|
||||||
) -> Result<Module> {
|
|
||||||
let content = std::fs::read_to_string(path)?;
|
|
||||||
eval_content(path, content, settings, repo_root, Globals::standard())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Path of the implicit helper library auto-loaded into every recipe.
|
|
||||||
pub const COMMON_LIB_MODULE: &str = "//lib:common.star";
|
|
||||||
const COMMON_LIB_RELATIVE: &str = "lib/common.star";
|
|
||||||
const OPTIONS_NAME: &str = "OPTIONS";
|
|
||||||
|
|
||||||
/// Names exported by `lib/common.star`, if the file exists. Empty otherwise.
|
|
||||||
pub fn common_lib_names(repo_root: &Path, settings: Option<&Config>) -> Result<Vec<String>> {
|
|
||||||
let path = repo_root.join(COMMON_LIB_RELATIVE);
|
|
||||||
if !path.exists() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
let module = eval_file(&path, settings, Some(repo_root))?;
|
|
||||||
Ok(module
|
|
||||||
.names()
|
|
||||||
.map(|n| n.as_str().to_owned())
|
|
||||||
.filter(|n| !n.starts_with('_') && n != "settings" && n != OPTIONS_NAME)
|
|
||||||
.collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Prepend an implicit `load("//lib:common.star", ...)` so every recipe sees
|
|
||||||
/// the shared helpers without an explicit import. Does nothing if there's no
|
|
||||||
/// `lib/common.star` or no repo root.
|
|
||||||
pub fn prepend_common_lib_load(
|
|
||||||
repo_root: Option<&Path>,
|
|
||||||
settings: Option<&Config>,
|
|
||||||
content: &str,
|
|
||||||
) -> Result<String> {
|
|
||||||
let Some(root) = repo_root else {
|
|
||||||
return Ok(content.to_owned());
|
|
||||||
};
|
|
||||||
let names = common_lib_names(root, settings)?;
|
|
||||||
if names.is_empty() {
|
|
||||||
return Ok(content.to_owned());
|
|
||||||
}
|
|
||||||
let names_lit = names
|
|
||||||
.iter()
|
|
||||||
.map(|n| format!("{:?}", n))
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ");
|
|
||||||
Ok(format!(
|
|
||||||
"load(\"{COMMON_LIB_MODULE}\", {names_lit})\n{content}"
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_content(
|
|
||||||
path: &Path,
|
|
||||||
content: String,
|
|
||||||
settings: Option<&Config>,
|
|
||||||
repo_root: Option<&Path>,
|
|
||||||
globals: Globals,
|
|
||||||
) -> Result<Module> {
|
|
||||||
eval_content_with_extra(path, content, settings, repo_root, globals, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_content_with_extra<'a>(
|
|
||||||
path: &Path,
|
|
||||||
content: String,
|
|
||||||
settings: Option<&Config>,
|
|
||||||
repo_root: Option<&Path>,
|
|
||||||
globals: Globals,
|
|
||||||
extra: Option<&'a dyn AnyLifetime<'a>>,
|
|
||||||
) -> Result<Module> {
|
|
||||||
let filename = path.display().to_string();
|
|
||||||
validate_options_source(path, &content)?;
|
|
||||||
let ast = AstModule::parse(
|
|
||||||
&filename,
|
|
||||||
content,
|
|
||||||
&Dialect {
|
|
||||||
enable_f_strings: true,
|
|
||||||
enable_top_level_stmt: true,
|
|
||||||
..Dialect::Standard
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.map_err(|err| anyhow!("{err}"))?;
|
|
||||||
let loader = match repo_root {
|
|
||||||
Some(root) => Some(RepoFileLoader::new(root, settings, globals.clone(), &ast)?),
|
|
||||||
None if !ast.loads().is_empty() => {
|
|
||||||
bail!(
|
|
||||||
"load() requires a repo root while evaluating {}",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
let module = Module::new();
|
|
||||||
let protected_options = if let Some(config) = settings {
|
|
||||||
Some(set_config_values(&module, config)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
{
|
|
||||||
let mut eval = Evaluator::new(&module);
|
|
||||||
if let Some(loader) = &loader {
|
|
||||||
eval.set_loader(loader);
|
|
||||||
}
|
|
||||||
eval.extra = extra;
|
|
||||||
eval.eval_module(ast, &globals)
|
|
||||||
.map_err(|err| anyhow!("{err}"))?;
|
|
||||||
}
|
|
||||||
validate_options_binding(path, &module, protected_options)?;
|
|
||||||
Ok(module)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_options_source(path: &Path, content: &str) -> Result<()> {
|
|
||||||
let line_offset = implicit_common_load_line_offset(content);
|
|
||||||
for (index, line) in content.lines().enumerate() {
|
|
||||||
let code = line.split('#').next().unwrap_or_default().trim_start();
|
|
||||||
if defines_or_reassigns_options(code) {
|
|
||||||
let line = (index + 1).saturating_sub(line_offset).max(1);
|
|
||||||
bail!(
|
|
||||||
"{}:{} must not define or reassign `{OPTIONS_NAME}`",
|
|
||||||
path.display(),
|
|
||||||
line
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn implicit_common_load_line_offset(content: &str) -> usize {
|
|
||||||
content
|
|
||||||
.lines()
|
|
||||||
.next()
|
|
||||||
.is_some_and(|line| line.starts_with(&format!("load(\"{COMMON_LIB_MODULE}\",")))
|
|
||||||
as usize
|
|
||||||
}
|
|
||||||
|
|
||||||
fn defines_or_reassigns_options(code: &str) -> bool {
|
|
||||||
let Some(rest) = code.strip_prefix(OPTIONS_NAME) else {
|
|
||||||
return defines_options_function(code) || binds_options_loop_variable(code);
|
|
||||||
};
|
|
||||||
let rest = rest.trim_start();
|
|
||||||
rest.starts_with('=')
|
|
||||||
|| rest.starts_with("+=")
|
|
||||||
|| rest.starts_with("-=")
|
|
||||||
|| rest.starts_with("*=")
|
|
||||||
|| rest.starts_with("/=")
|
|
||||||
|| rest.starts_with("%=")
|
|
||||||
|| rest.starts_with("&=")
|
|
||||||
|| rest.starts_with("|=")
|
|
||||||
|| rest.starts_with("^=")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn defines_options_function(code: &str) -> bool {
|
|
||||||
code.strip_prefix("def ")
|
|
||||||
.and_then(|rest| rest.trim_start().strip_prefix(OPTIONS_NAME))
|
|
||||||
.is_some_and(|rest| rest.trim_start().starts_with('('))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn binds_options_loop_variable(code: &str) -> bool {
|
|
||||||
code.strip_prefix("for ")
|
|
||||||
.and_then(|rest| rest.trim_start().strip_prefix(OPTIONS_NAME))
|
|
||||||
.is_some_and(|rest| rest.trim_start().starts_with("in "))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_options_binding(
|
|
||||||
path: &Path,
|
|
||||||
module: &Module,
|
|
||||||
expected: Option<Value<'_>>,
|
|
||||||
) -> Result<()> {
|
|
||||||
let Some(expected) = expected else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
let actual = module.get(OPTIONS_NAME).ok_or_else(|| {
|
|
||||||
anyhow!(
|
|
||||||
"{} removed the protected `{OPTIONS_NAME}` binding",
|
|
||||||
path.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
if !actual.ptr_eq(expected) {
|
|
||||||
bail!(
|
|
||||||
"{} must not define or reassign `{OPTIONS_NAME}`",
|
|
||||||
path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
struct RepoFileLoader {
|
|
||||||
modules: HashMap<String, FrozenModule>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RepoFileLoader {
|
|
||||||
fn new(
|
|
||||||
repo_root: &Path,
|
|
||||||
settings: Option<&Config>,
|
|
||||||
globals: Globals,
|
|
||||||
ast: &AstModule,
|
|
||||||
) -> Result<Self> {
|
|
||||||
let mut modules = HashMap::new();
|
|
||||||
for load in ast.loads() {
|
|
||||||
load_module(
|
|
||||||
repo_root,
|
|
||||||
settings,
|
|
||||||
globals.clone(),
|
|
||||||
load.module_id,
|
|
||||||
&mut modules,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
Ok(Self { modules })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FileLoader for RepoFileLoader {
|
|
||||||
fn load(&self, path: &str) -> starlark::Result<FrozenModule> {
|
|
||||||
self.modules
|
|
||||||
.get(path)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| starlark::Error::new_other(anyhow!("unknown Starlark module `{path}`")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_module(
|
|
||||||
repo_root: &Path,
|
|
||||||
settings: Option<&Config>,
|
|
||||||
globals: Globals,
|
|
||||||
module_id: &str,
|
|
||||||
modules: &mut HashMap<String, FrozenModule>,
|
|
||||||
) -> Result<()> {
|
|
||||||
if modules.contains_key(module_id) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
let path = resolve_load_path(repo_root, module_id)?;
|
|
||||||
let content = std::fs::read_to_string(&path)?;
|
|
||||||
let filename = path.display().to_string();
|
|
||||||
validate_options_source(&path, &content)?;
|
|
||||||
let ast =
|
|
||||||
AstModule::parse(&filename, content, &Dialect::Standard).map_err(|err| anyhow!("{err}"))?;
|
|
||||||
for load in ast.loads() {
|
|
||||||
load_module(
|
|
||||||
repo_root,
|
|
||||||
settings,
|
|
||||||
globals.clone(),
|
|
||||||
load.module_id,
|
|
||||||
modules,
|
|
||||||
)?;
|
|
||||||
}
|
|
||||||
let nested_loader = RepoFileLoader {
|
|
||||||
modules: modules.clone(),
|
|
||||||
};
|
|
||||||
let module = Module::new();
|
|
||||||
let protected_options = if let Some(config) = settings {
|
|
||||||
Some(set_config_values(&module, config)?)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
{
|
|
||||||
let mut eval = Evaluator::new(&module);
|
|
||||||
eval.set_loader(&nested_loader);
|
|
||||||
eval.eval_module(ast, &globals)
|
|
||||||
.map_err(|err| anyhow!("{err}"))?;
|
|
||||||
}
|
|
||||||
validate_options_binding(&path, &module, protected_options)?;
|
|
||||||
let frozen = module.freeze().map_err(|err| anyhow!("{err:?}"))?;
|
|
||||||
modules.insert(module_id.to_owned(), frozen);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_config_values<'v>(module: &'v Module, config: &Config) -> Result<Value<'v>> {
|
|
||||||
module.set("settings", module.heap().alloc(SettingsValue::from(config)));
|
|
||||||
let options = module
|
|
||||||
.heap()
|
|
||||||
.alloc(OptionsValue::new(config.options.clone())?);
|
|
||||||
module.set(OPTIONS_NAME, options);
|
|
||||||
Ok(options)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_starlark_identifier(name: &str) -> Result<()> {
|
|
||||||
let mut chars = name.chars();
|
|
||||||
let Some(first) = chars.next() else {
|
|
||||||
bail!("config option name cannot be empty");
|
|
||||||
};
|
|
||||||
if !(first == '_' || first.is_ascii_alphabetic()) {
|
|
||||||
bail!("config option `{name}` is not a valid Starlark identifier");
|
|
||||||
}
|
|
||||||
if chars.any(|ch| !(ch == '_' || ch.is_ascii_alphanumeric())) {
|
|
||||||
bail!("config option `{name}` is not a valid Starlark identifier");
|
|
||||||
}
|
|
||||||
if matches!(
|
|
||||||
name,
|
|
||||||
"and"
|
|
||||||
| "as"
|
|
||||||
| "assert"
|
|
||||||
| "break"
|
|
||||||
| "class"
|
|
||||||
| "continue"
|
|
||||||
| "def"
|
|
||||||
| "del"
|
|
||||||
| "elif"
|
|
||||||
| "else"
|
|
||||||
| "except"
|
|
||||||
| "finally"
|
|
||||||
| "for"
|
|
||||||
| "from"
|
|
||||||
| "global"
|
|
||||||
| "if"
|
|
||||||
| "import"
|
|
||||||
| "in"
|
|
||||||
| "is"
|
|
||||||
| "lambda"
|
|
||||||
| "load"
|
|
||||||
| "not"
|
|
||||||
| "or"
|
|
||||||
| "pass"
|
|
||||||
| "return"
|
|
||||||
| "try"
|
|
||||||
| "while"
|
|
||||||
| "with"
|
|
||||||
| "yield"
|
|
||||||
) {
|
|
||||||
bail!("config option `{name}` is a reserved Starlark keyword");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_load_path(repo_root: &Path, module_id: &str) -> Result<PathBuf> {
|
|
||||||
let relative = if let Some(stripped) = module_id.strip_prefix("//") {
|
|
||||||
stripped.replace(':', "/")
|
|
||||||
} else {
|
|
||||||
module_id.to_owned()
|
|
||||||
};
|
|
||||||
let path = repo_root.join(relative);
|
|
||||||
let canonical_root = repo_root
|
|
||||||
.canonicalize()
|
|
||||||
.unwrap_or_else(|_| repo_root.to_path_buf());
|
|
||||||
let canonical_path = path.canonicalize().unwrap_or(path);
|
|
||||||
if !canonical_path.starts_with(&canonical_root) {
|
|
||||||
bail!("Starlark load escapes repo root: {module_id}");
|
|
||||||
}
|
|
||||||
Ok(canonical_path)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn option_value_to_string(value: &JsonValue) -> String {
|
|
||||||
match value {
|
|
||||||
JsonValue::String(value) => value.clone(),
|
|
||||||
JsonValue::Bool(value) => value.to_string(),
|
|
||||||
JsonValue::Number(value) => value.to_string(),
|
|
||||||
JsonValue::Null => "null".to_owned(),
|
|
||||||
JsonValue::Array(_) | JsonValue::Object(_) => value.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_string(module: &Module, name: &str) -> Result<String> {
|
|
||||||
module
|
|
||||||
.get(name)
|
|
||||||
.and_then(|v| v.unpack_str().map(ToOwned::to_owned))
|
|
||||||
.ok_or_else(|| anyhow!("missing or non-string Starlark variable `{name}`"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_string_default(module: &Module, name: &str, default: &str) -> Result<String> {
|
|
||||||
Ok(match module.get(name) {
|
|
||||||
Some(value) => value
|
|
||||||
.unpack_str()
|
|
||||||
.ok_or_else(|| anyhow!("non-string Starlark variable `{name}`"))?
|
|
||||||
.to_owned(),
|
|
||||||
None => default.to_owned(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_i32_default(module: &Module, name: &str, default: i32) -> Result<i32> {
|
|
||||||
Ok(match module.get(name) {
|
|
||||||
Some(value) => value
|
|
||||||
.unpack_i32()
|
|
||||||
.ok_or_else(|| anyhow!("non-integer Starlark variable `{name}`"))?,
|
|
||||||
None => default,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn has_name(module: &Module, name: &str) -> bool {
|
|
||||||
module.get(name).is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_json(module: &Module, name: &str) -> Result<Option<JsonValue>> {
|
|
||||||
match module.get(name) {
|
|
||||||
Some(value) => Ok(Some(serde_json::from_str(&value.to_json()?)?)),
|
|
||||||
None => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_string_vec(module: &Module, name: &str) -> Result<Vec<String>> {
|
|
||||||
match get_json(module, name)? {
|
|
||||||
Some(JsonValue::Array(values)) => values
|
|
||||||
.into_iter()
|
|
||||||
.map(|value| match value {
|
|
||||||
JsonValue::String(s) => Ok(s),
|
|
||||||
_ => bail!("`{name}` must be a list of strings"),
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
Some(_) => bail!("`{name}` must be a list of strings"),
|
|
||||||
None => Ok(Vec::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_json_map(module: &Module, name: &str) -> Result<BTreeMap<String, JsonValue>> {
|
|
||||||
match get_json(module, name)? {
|
|
||||||
Some(JsonValue::Object(values)) => Ok(values.into_iter().collect()),
|
|
||||||
Some(_) => bail!("`{name}` must be a dict"),
|
|
||||||
None => Ok(BTreeMap::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-256
@@ -1,256 +0,0 @@
|
|||||||
//! `distro update` — check repology for newer upstream versions and bump
|
|
||||||
//! recipes in-place.
|
|
||||||
//!
|
|
||||||
//! For each requested recipe we query
|
|
||||||
//! `https://repology.org/api/v1/project/<name>` and pick the highest version
|
|
||||||
//! among entries whose `status` is `"newest"` or `"unique"` (i.e. not
|
|
||||||
//! outdated, not rolling/devel, not ignored). If that version is strictly
|
|
||||||
//! greater than the recipe's current `version`, we rewrite:
|
|
||||||
//!
|
|
||||||
//! * `version = "..."` → the new upstream version
|
|
||||||
//! * `revision = N` → `revision = 1`
|
|
||||||
//! * every `"sha256": "..."` → `"sha256": "???"` so the next fetch fills it
|
|
||||||
//!
|
|
||||||
//! Repology asks API clients to set a descriptive User-Agent.
|
|
||||||
|
|
||||||
use crate::log;
|
|
||||||
use crate::recipe::{Recipe, RecipeSet};
|
|
||||||
use anyhow::{Context, Result, bail};
|
|
||||||
use serde::Deserialize;
|
|
||||||
use std::cmp::Ordering;
|
|
||||||
use std::fs;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
const REPOLOGY_API: &str = "https://repology.org/api/v1/project";
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
struct RepologyEntry {
|
|
||||||
#[serde(default)]
|
|
||||||
version: String,
|
|
||||||
#[serde(default)]
|
|
||||||
status: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run(recipes: &RecipeSet, names: &[String], bump: bool) -> Result<()> {
|
|
||||||
let targets: Vec<&Recipe> = if names.is_empty() {
|
|
||||||
recipes.recipes.values().collect()
|
|
||||||
} else {
|
|
||||||
let mut out = Vec::with_capacity(names.len());
|
|
||||||
for name in names {
|
|
||||||
out.push(recipes.recipe_by_user_ref(name)?);
|
|
||||||
}
|
|
||||||
out
|
|
||||||
};
|
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::builder()
|
|
||||||
.user_agent(concat!(
|
|
||||||
"distro/",
|
|
||||||
env!("CARGO_PKG_VERSION"),
|
|
||||||
" (+repology version checker)"
|
|
||||||
))
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
let mut outdated = 0usize;
|
|
||||||
let mut bumped = 0usize;
|
|
||||||
let mut up_to_date = 0usize;
|
|
||||||
let mut errored = 0usize;
|
|
||||||
|
|
||||||
for recipe in targets {
|
|
||||||
match check_one(&client, recipe) {
|
|
||||||
Ok(Some(new_version)) => {
|
|
||||||
if bump {
|
|
||||||
bump_recipe(&recipe.path, &new_version)?;
|
|
||||||
log::step(
|
|
||||||
"bump",
|
|
||||||
&format!("{}: {} -> {}", recipe.key(), recipe.version, new_version),
|
|
||||||
);
|
|
||||||
bumped += 1;
|
|
||||||
} else {
|
|
||||||
log::step(
|
|
||||||
"outdated",
|
|
||||||
&format!("{}: {} -> {}", recipe.key(), recipe.version, new_version),
|
|
||||||
);
|
|
||||||
outdated += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(None) => {
|
|
||||||
log::skip(
|
|
||||||
"up-to-date",
|
|
||||||
&format!("{} {}", recipe.key(), recipe.version),
|
|
||||||
);
|
|
||||||
up_to_date += 1;
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::info("error", &format!("{}: {e}", recipe.key()));
|
|
||||||
errored += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if bump {
|
|
||||||
log::step(
|
|
||||||
"summary",
|
|
||||||
&format!("{bumped} bumped, {up_to_date} up-to-date, {errored} errored"),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
log::step(
|
|
||||||
"summary",
|
|
||||||
&format!(
|
|
||||||
"{outdated} outdated, {up_to_date} up-to-date, {errored} errored (re-run with --bump to rewrite recipes)"
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn check_one(client: &reqwest::blocking::Client, recipe: &Recipe) -> Result<Option<String>> {
|
|
||||||
let url = format!("{REPOLOGY_API}/{}", recipe.name);
|
|
||||||
let resp = client
|
|
||||||
.get(&url)
|
|
||||||
.send()
|
|
||||||
.with_context(|| format!("GET {url}"))?;
|
|
||||||
if resp.status() == reqwest::StatusCode::NOT_FOUND {
|
|
||||||
bail!("repology has no project named `{}`", recipe.name);
|
|
||||||
}
|
|
||||||
let entries: Vec<RepologyEntry> = serde_json::from_slice(&resp.error_for_status()?.bytes()?)
|
|
||||||
.context("failed to parse repology response")?;
|
|
||||||
let latest = entries
|
|
||||||
.iter()
|
|
||||||
.filter(|e| matches!(e.status.as_str(), "newest" | "unique"))
|
|
||||||
.map(|e| e.version.as_str())
|
|
||||||
.max_by(|a, b| natural_cmp(a, b));
|
|
||||||
match latest {
|
|
||||||
Some(v) if natural_cmp(v, &recipe.version) == Ordering::Greater => Ok(Some(v.to_owned())),
|
|
||||||
_ => Ok(None),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bump_recipe(path: &Path, new_version: &str) -> Result<()> {
|
|
||||||
let original =
|
|
||||||
fs::read_to_string(path).with_context(|| format!("failed to read {}", path.display()))?;
|
|
||||||
let mut out = String::with_capacity(original.len());
|
|
||||||
let mut version_replaced = false;
|
|
||||||
let mut revision_replaced = false;
|
|
||||||
for line in original.split_inclusive('\n') {
|
|
||||||
let trimmed = line.trim_start();
|
|
||||||
if !version_replaced && trimmed.starts_with("version") {
|
|
||||||
if let Some(replaced) = replace_string_assignment(line, "version", new_version) {
|
|
||||||
out.push_str(&replaced);
|
|
||||||
version_replaced = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !revision_replaced && trimmed.starts_with("revision") {
|
|
||||||
if let Some(replaced) = replace_int_assignment(line, "revision", 1) {
|
|
||||||
out.push_str(&replaced);
|
|
||||||
revision_replaced = true;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push_str(line);
|
|
||||||
}
|
|
||||||
if !version_replaced {
|
|
||||||
bail!("could not find `version = \"...\"` in {}", path.display());
|
|
||||||
}
|
|
||||||
// Reset every sha256 placeholder so the next fetch re-derives it.
|
|
||||||
out = out.replace("\"sha256\": \"", "\x00sha256_marker\x00\"");
|
|
||||||
out = regex_lite_replace_sha(&out);
|
|
||||||
out = out.replace("\x00sha256_marker\x00\"", "\"sha256\": \"");
|
|
||||||
fs::write(path, out).with_context(|| format!("failed to write {}", path.display()))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Replace every quoted sha256 value with `"???"`, leaving keys/whitespace
|
|
||||||
/// alone. Avoids a full regex dep by walking the string by hand.
|
|
||||||
fn regex_lite_replace_sha(input: &str) -> String {
|
|
||||||
let mut out = String::with_capacity(input.len());
|
|
||||||
let bytes = input.as_bytes();
|
|
||||||
let needle = b"\x00sha256_marker\x00\"";
|
|
||||||
let mut i = 0;
|
|
||||||
while i < bytes.len() {
|
|
||||||
if bytes[i..].starts_with(needle) {
|
|
||||||
out.push_str("\x00sha256_marker\x00\"???\"");
|
|
||||||
i += needle.len();
|
|
||||||
// skip the original value up to the closing quote
|
|
||||||
while i < bytes.len() && bytes[i] != b'"' {
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
if i < bytes.len() {
|
|
||||||
i += 1; // consume the closing quote we replaced
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
out.push(bytes[i] as char);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_string_assignment(line: &str, key: &str, new_value: &str) -> Option<String> {
|
|
||||||
// Match `<key><space>=<space>"value"` while preserving surrounding text
|
|
||||||
// (indentation, trailing newline).
|
|
||||||
let stripped = line.strip_prefix(key)?;
|
|
||||||
let rest = stripped.trim_start_matches(|c: char| c == ' ' || c == '\t');
|
|
||||||
let after_eq = rest.strip_prefix('=')?.trim_start();
|
|
||||||
let after_quote = after_eq.strip_prefix('"')?;
|
|
||||||
let end = after_quote.find('"')?;
|
|
||||||
let trailing = &after_quote[end + 1..];
|
|
||||||
Some(format!("{key} = \"{new_value}\"{trailing}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn replace_int_assignment(line: &str, key: &str, new_value: i32) -> Option<String> {
|
|
||||||
let stripped = line.strip_prefix(key)?;
|
|
||||||
let rest = stripped.trim_start_matches(|c: char| c == ' ' || c == '\t');
|
|
||||||
let after_eq = rest.strip_prefix('=')?.trim_start();
|
|
||||||
let end = after_eq.find(|c: char| !c.is_ascii_digit())?;
|
|
||||||
let trailing = &after_eq[end..];
|
|
||||||
Some(format!("{key} = {new_value}{trailing}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// dpkg-ish natural comparison: split into runs of digits and non-digits and
|
|
||||||
/// compare numerically where both sides are digits, lexicographically
|
|
||||||
/// otherwise. Good enough for upstream tarball versions.
|
|
||||||
fn natural_cmp(a: &str, b: &str) -> Ordering {
|
|
||||||
let mut ai = a.chars().peekable();
|
|
||||||
let mut bi = b.chars().peekable();
|
|
||||||
loop {
|
|
||||||
match (ai.peek().copied(), bi.peek().copied()) {
|
|
||||||
(None, None) => return Ordering::Equal,
|
|
||||||
(None, _) => return Ordering::Less,
|
|
||||||
(_, None) => return Ordering::Greater,
|
|
||||||
(Some(x), Some(y)) if x.is_ascii_digit() && y.is_ascii_digit() => {
|
|
||||||
let mut na = String::new();
|
|
||||||
while let Some(&c) = ai.peek() {
|
|
||||||
if c.is_ascii_digit() {
|
|
||||||
na.push(c);
|
|
||||||
ai.next();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let mut nb = String::new();
|
|
||||||
while let Some(&c) = bi.peek() {
|
|
||||||
if c.is_ascii_digit() {
|
|
||||||
nb.push(c);
|
|
||||||
bi.next();
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let xa: u64 = na.parse().unwrap_or(0);
|
|
||||||
let xb: u64 = nb.parse().unwrap_or(0);
|
|
||||||
match xa.cmp(&xb) {
|
|
||||||
Ordering::Equal => continue,
|
|
||||||
other => return other,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(Some(x), Some(y)) => match x.cmp(&y) {
|
|
||||||
Ordering::Equal => {
|
|
||||||
ai.next();
|
|
||||||
bi.next();
|
|
||||||
}
|
|
||||||
other => return other,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user