Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e6704516a | |||
| 1a7c817fb9 | |||
| a525868969 |
@@ -1,4 +0,0 @@
|
|||||||
target
|
|
||||||
build
|
|
||||||
.git
|
|
||||||
*.bak
|
|
||||||
@@ -1,4 +1,2 @@
|
|||||||
/target
|
/target
|
||||||
/build
|
/build
|
||||||
*.bak
|
|
||||||
*.lock
|
|
||||||
|
|||||||
Generated
+47
-1133
File diff suppressed because it is too large
Load Diff
+10
-18
@@ -1,23 +1,15 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "distro"
|
name = "builder"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT"
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0"
|
allocative = "0.3.4"
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
anyhow = "1.0.102"
|
||||||
hex = "0.4"
|
clap = { version = "4.6.1", features = ["derive"] }
|
||||||
reqwest = { version = "0.12", default-features = false, features = [
|
either = "1.16.0"
|
||||||
"blocking",
|
petgraph = "0.8.3"
|
||||||
"rustls-tls",
|
smallvec = "1.15.1"
|
||||||
] }
|
starlark = "0.13.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
starlark_derive = "0.13.0"
|
||||||
serde_json = "1.0"
|
thiserror = "2.0.18"
|
||||||
sha2 = "0.10"
|
|
||||||
shell-escape = "0.1"
|
|
||||||
starlark = "0.13"
|
|
||||||
starlark_derive = "0.13"
|
|
||||||
allocative = "0.3"
|
|
||||||
tempfile = "3.10"
|
|
||||||
walkdir = "2.5"
|
|
||||||
|
|||||||
-40
@@ -1,40 +0,0 @@
|
|||||||
FROM alpine:edge
|
|
||||||
|
|
||||||
RUN apk upgrade --no-cache && apk add --no-cache \
|
|
||||||
alpine-sdk \
|
|
||||||
apk-tools \
|
|
||||||
autoconf \
|
|
||||||
automake \
|
|
||||||
bash \
|
|
||||||
bc \
|
|
||||||
bison \
|
|
||||||
bzip2 \
|
|
||||||
ca-certificates \
|
|
||||||
cmake \
|
|
||||||
coreutils \
|
|
||||||
curl \
|
|
||||||
file \
|
|
||||||
findutils \
|
|
||||||
flex \
|
|
||||||
gettext-dev \
|
|
||||||
git \
|
|
||||||
gzip \
|
|
||||||
elfutils-dev \
|
|
||||||
gmp-dev \
|
|
||||||
mpfr-dev \
|
|
||||||
mpc1-dev \
|
|
||||||
libtool \
|
|
||||||
linux-headers \
|
|
||||||
meson \
|
|
||||||
ninja \
|
|
||||||
openssl \
|
|
||||||
openssl-dev \
|
|
||||||
patch \
|
|
||||||
pkgconf \
|
|
||||||
python3 \
|
|
||||||
tar \
|
|
||||||
texinfo \
|
|
||||||
xz \
|
|
||||||
zstd
|
|
||||||
|
|
||||||
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.
|
|
||||||
+51
-23
@@ -1,36 +1,64 @@
|
|||||||
container_runtime = "podman"
|
|
||||||
container_image = "local/distro-builder:latest"
|
|
||||||
container_dockerfile = "Dockerfile"
|
|
||||||
|
|
||||||
arch = "x86_64"
|
arch = "x86_64"
|
||||||
libc = "musl"
|
libc = "glibc"
|
||||||
|
|
||||||
host_cflags = "-O2 -pipe"
|
if libc == "glibc":
|
||||||
host_cxxflags = ""
|
env = "gnu"
|
||||||
host_ldflags = "-Wl,-O1 -Wl,--sort-common -Wl,--as-needed"
|
elif libc == "musl":
|
||||||
|
env = "musl"
|
||||||
|
else:
|
||||||
|
fail(f"Unknown libc: {libc}")
|
||||||
|
|
||||||
target_cflags = host_cflags
|
prefix = path("/usr")
|
||||||
target_cxxflags = host_cxxflags
|
|
||||||
target_ldflags = host_ldflags + " -Wl,-z,now"
|
host_cflags = ["-O2", "-pipe"]
|
||||||
|
host_cxxflags = host_cflags + []
|
||||||
|
host_ldflags = ["-Wl,-O1", "-Wl,--sort-common", "-Wl,--as-needed"]
|
||||||
|
|
||||||
|
target_cflags = host_cflags + []
|
||||||
|
target_cxxflags = host_cxxflags + []
|
||||||
|
target_ldflags = host_ldflags + ["-Wl,-z,now"]
|
||||||
|
|
||||||
if arch == "x86_64":
|
if arch == "x86_64":
|
||||||
flags = " -march=x86-64-v3 -mtune=generic -fstack-clash-protection -fstack-protector-strong -fcf-protection"
|
flags = [
|
||||||
|
"-march=x86-64-v3",
|
||||||
|
"-mtune=generic",
|
||||||
|
"-fstack-clash-protection",
|
||||||
|
"-fstack-protector-strong",
|
||||||
|
"-fcf-protection",
|
||||||
|
]
|
||||||
|
|
||||||
target_cflags += flags
|
target_cflags += flags
|
||||||
target_cxxflags += flags
|
target_cxxflags += flags
|
||||||
target_ldflags += " -Wl,-z,pack-relative-relocs"
|
target_ldflags += ["-Wl,-z,pack-relative-relocs"]
|
||||||
|
|
||||||
|
config(
|
||||||
|
arch = arch,
|
||||||
|
recipes_dir = path("./recipes"),
|
||||||
|
host_recipes_dir = path("./host-recipes"),
|
||||||
|
container = podman(
|
||||||
|
image = "local/builder:latest",
|
||||||
|
dockerfile = path("./Dockerfile"),
|
||||||
|
),
|
||||||
|
|
||||||
options = dict(
|
|
||||||
target_arch = arch,
|
target_arch = arch,
|
||||||
target_triple = f"{arch}-linux-{libc}",
|
target_triple = f"{arch}-orchid-linux-{env}",
|
||||||
|
|
||||||
host_cflags = host_cflags,
|
|
||||||
host_cxxflags = host_cxxflags,
|
|
||||||
host_ldflags = host_ldflags,
|
|
||||||
|
|
||||||
cflags = target_cflags,
|
|
||||||
cxxflags = target_cxxflags,
|
|
||||||
ldflags = target_ldflags,
|
|
||||||
|
|
||||||
libc = libc,
|
libc = libc,
|
||||||
|
|
||||||
|
prefix = prefix,
|
||||||
|
bindir = prefix / "bin",
|
||||||
|
sbindir = prefix / "bin",
|
||||||
|
libdir = prefix / "lib",
|
||||||
|
libexecdir = prefix / "libexec",
|
||||||
|
includedir = prefix / "include",
|
||||||
|
sysconfdir = path("/etc"),
|
||||||
|
localstatedir = path("/var"),
|
||||||
|
|
||||||
|
host_cflags = " ".join(host_cflags),
|
||||||
|
host_cxxflags = " ".join(host_cxxflags),
|
||||||
|
host_ldflags = " ".join(host_ldflags),
|
||||||
|
|
||||||
|
cflags = " ".join(target_cflags),
|
||||||
|
cxxflags = " ".join(target_cxxflags),
|
||||||
|
ldflags = " ".join(target_ldflags),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,18 +4,18 @@ metadata = meta(
|
|||||||
description = "GNU binutils cross-compiled for the target triple",
|
description = "GNU binutils cross-compiled for the target triple",
|
||||||
license = "GPL-3.0-or-later",
|
license = "GPL-3.0-or-later",
|
||||||
)
|
)
|
||||||
source = tarball_source(
|
source = tarball(
|
||||||
url = "https://ftp.gnu.org/gnu/binutils/binutils-" + version + ".tar.xz",
|
url = f"https://ftp.gnu.org/gnu/binutils/binutils-{version}.tar.xz",
|
||||||
sha256 = "d75a94f4d73e7a4086f7513e67e439e8fcdcbb726ffe63f4661744e6256b2cf2",
|
sha256 = "d75a94f4d73e7a4086f7513e67e439e8fcdcbb726ffe63f4661744e6256b2cf2",
|
||||||
strip_components = 1,
|
strip_components = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
def configure(ctx):
|
def configure(ctx):
|
||||||
ctx.run([
|
ctx.run(
|
||||||
ctx.source_dir + "/configure",
|
ctx.source_dir / "configure",
|
||||||
"--prefix=" + ctx.prefix,
|
"--prefix=" + options.prefix,
|
||||||
"--target=" + options.target_triple,
|
"--target=" + options.target_triple,
|
||||||
"--with-sysroot=" + ctx.sysroot,
|
"--with-sysroot=" + ctx.sysroot_dir,
|
||||||
"--with-pic",
|
"--with-pic",
|
||||||
"--enable-cet",
|
"--enable-cet",
|
||||||
"--enable-default-execstack=no",
|
"--enable-default-execstack=no",
|
||||||
@@ -26,11 +26,11 @@ def configure(ctx):
|
|||||||
"--enable-relro",
|
"--enable-relro",
|
||||||
"--enable-separate-code",
|
"--enable-separate-code",
|
||||||
"--enable-threads",
|
"--enable-threads",
|
||||||
# gprofng's libcollector does not build against musl.
|
|
||||||
"--disable-gprofng",
|
|
||||||
"--disable-nls",
|
"--disable-nls",
|
||||||
"--disable-werror",
|
"--disable-werror",
|
||||||
], env = {
|
# gprofng's libcollector relies on glibc-specific internals.
|
||||||
|
"--disable-gprofng",
|
||||||
|
env = {
|
||||||
"CFLAGS": options.host_cflags,
|
"CFLAGS": options.host_cflags,
|
||||||
"CXXFLAGS": options.host_cxxflags,
|
"CXXFLAGS": options.host_cxxflags,
|
||||||
"LDFLAGS": options.host_ldflags,
|
"LDFLAGS": options.host_ldflags,
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
version = "16.1.0"
|
|
||||||
revision = 1
|
|
||||||
metadata = meta(
|
|
||||||
description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)",
|
|
||||||
license = "GPL-3.0-or-later",
|
|
||||||
)
|
|
||||||
source = tarball_source(
|
|
||||||
url = f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
|
|
||||||
sha256 = "50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
|
|
||||||
strip_components = 1,
|
|
||||||
)
|
|
||||||
host_deps = ["binutils"]
|
|
||||||
|
|
||||||
def configure(ctx):
|
|
||||||
ctx.run([
|
|
||||||
ctx.source_dir + "/configure",
|
|
||||||
"--target=" + options.target_triple,
|
|
||||||
"--prefix=" + ctx.prefix,
|
|
||||||
"--with-sysroot=" + ctx.sysroot,
|
|
||||||
"--without-headers",
|
|
||||||
"--with-newlib",
|
|
||||||
"--enable-languages=c,c++",
|
|
||||||
"--enable-default-pie",
|
|
||||||
"--enable-default-ssp",
|
|
||||||
"--disable-nls",
|
|
||||||
"--disable-shared",
|
|
||||||
"--disable-threads",
|
|
||||||
"--disable-libssp",
|
|
||||||
"--disable-libgomp",
|
|
||||||
"--disable-libquadmath",
|
|
||||||
"--disable-libatomic",
|
|
||||||
"--disable-libvtv",
|
|
||||||
"--disable-multilib",
|
|
||||||
], env = {
|
|
||||||
"CFLAGS": options.host_cflags,
|
|
||||||
"CXXFLAGS": options.host_cxxflags,
|
|
||||||
"LDFLAGS": options.host_ldflags,
|
|
||||||
})
|
|
||||||
|
|
||||||
def build(ctx):
|
|
||||||
jobs = "-j" + str(ctx.jobs)
|
|
||||||
ctx.run(["make", jobs, "all-gcc"])
|
|
||||||
ctx.run(["make", jobs, "all-target-libgcc"])
|
|
||||||
|
|
||||||
def install(ctx, pkg):
|
|
||||||
ctx.run(["make", "install-gcc"], env = {"DESTDIR": pkg.dest_dir})
|
|
||||||
ctx.run(["make", "install-target-libgcc"], env = {"DESTDIR": pkg.dest_dir})
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
# Commonly used helpers.
|
|
||||||
|
|
||||||
def autotools_configure(ctx, extra_args = [], extra_env = {}):
|
|
||||||
env = {
|
|
||||||
"CFLAGS": options.cflags,
|
|
||||||
"CXXFLAGS": options.cxxflags,
|
|
||||||
"LDFLAGS": options.ldflags,
|
|
||||||
}
|
|
||||||
env.update(extra_env)
|
|
||||||
ctx.run([
|
|
||||||
ctx.source_dir + "/configure",
|
|
||||||
"--host=" + options.target_triple,
|
|
||||||
"--with-sysroot=" + ctx.sysroot,
|
|
||||||
"--prefix=" + ctx.prefix,
|
|
||||||
"--sysconfdir=/etc",
|
|
||||||
"--localstatedir=/var",
|
|
||||||
"--bindir=" + ctx.prefix + "/bin",
|
|
||||||
"--sbindir=" + ctx.prefix + "/bin",
|
|
||||||
"--libdir=" + ctx.prefix + "/lib",
|
|
||||||
"--disable-static",
|
|
||||||
"--enable-shared",
|
|
||||||
] + extra_args, env = env)
|
|
||||||
|
|
||||||
def autotools_build(ctx, extra_args = []):
|
|
||||||
ctx.run(["make", "-j" + str(ctx.jobs)] + extra_args)
|
|
||||||
|
|
||||||
def autotools_install(ctx, pkg, extra_args = []):
|
|
||||||
ctx.run(["make", "install"] + extra_args, env = {"DESTDIR": pkg.dest_dir})
|
|
||||||
|
|
||||||
def autotools(configure_args = [], configure_env = {}, build_args = [], install_args = []):
|
|
||||||
def _configure(ctx):
|
|
||||||
autotools_configure(ctx, extra_args = configure_args, extra_env = configure_env)
|
|
||||||
def _build(ctx):
|
|
||||||
autotools_build(ctx, extra_args = build_args)
|
|
||||||
def _install(ctx, pkg):
|
|
||||||
autotools_install(ctx, pkg, extra_args = install_args)
|
|
||||||
return _configure, _build, _install
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
version = "12.2.0"
|
|
||||||
revision = 1
|
|
||||||
metadata = meta(
|
|
||||||
description = "Modern, secure, portable, multiprotocol bootloader and boot manager",
|
|
||||||
license = "BSD-2-Clause",
|
|
||||||
)
|
|
||||||
source = tarball_source(
|
|
||||||
url = f"https://github.com/Limine-Bootloader/Limine/releases/download/v{version}/limine-{version}.tar.gz",
|
|
||||||
sha256 = "db8a119878cfeead63c0a78236c577c40539c5759496950ea0ed32a6cf567865",
|
|
||||||
strip_components = 1,
|
|
||||||
)
|
|
||||||
host_deps = ["binutils", "gcc"]
|
|
||||||
deps = [options.libc]
|
|
||||||
subpackages = [
|
|
||||||
subpackage(
|
|
||||||
name = "limine-bios",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
|
|
||||||
configure, build, install = autotools(configure_env = {
|
|
||||||
"TOOLCHAIN_FOR_TARGET": options.target_triple + "-",
|
|
||||||
"LD_FOR_TARGET": options.target_triple + "-" + "ld",
|
|
||||||
"OBJCOPY_FOR_TARGET": options.target_triple + "-" + "objcopy",
|
|
||||||
"OBJDUMP_FOR_TARGET": options.target_triple + "-" + "objdump",
|
|
||||||
})
|
|
||||||
@@ -4,17 +4,17 @@ metadata = meta(
|
|||||||
description = "Linux kernel headers for userspace development",
|
description = "Linux kernel headers for userspace development",
|
||||||
license = "GPL-2.0-only",
|
license = "GPL-2.0-only",
|
||||||
)
|
)
|
||||||
source = tarball_source(
|
source = tarball(
|
||||||
url = f"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{version}.tar.xz",
|
url = f"https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-{version}.tar.xz",
|
||||||
sha256 = "ac07acdf76cf4621cc5187a2670270a1a699533c8a6b225e4878c416ad83f1c4",
|
sha256 = "ac07acdf76cf4621cc5187a2670270a1a699533c8a6b225e4878c416ad83f1c4",
|
||||||
strip_components = 1,
|
strip_components = 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
def build(ctx):
|
def build(ctx):
|
||||||
ctx.run(["cp", "-rp", ctx.source_dir + "/.", ctx.build_dir])
|
ctx.run("cp", "-rp", ctx.source_dir / ".", ctx.build_dir)
|
||||||
ctx.run(["make", "headers_install", "ARCH=" + options.target_arch])
|
ctx.run("make", "headers_install", "ARCH=" + options.target_arch)
|
||||||
ctx.run(["find", ctx.build_dir + "/usr/include", "-type", "f", "!", "-name", "*.h", "-delete"])
|
ctx.run("find", ctx.build_dir / "usr" / "include", "-type", "f", "!", "-name", "*.h", "-delete")
|
||||||
|
|
||||||
def install(ctx, pkg):
|
def install(ctx):
|
||||||
ctx.run(["mkdir", "-p", pkg.dest_dir + ctx.prefix])
|
ctx.run("mkdir", "-p", ctx.dest_dir / options.prefix)
|
||||||
ctx.run(["cp", "-rp", ctx.build_dir + "/usr/include", pkg.dest_dir + ctx.prefix])
|
ctx.run("cp", "-rp", ctx.build_dir / "usr" / "include", ctx.dest_dir / options.prefix)
|
||||||
|
|||||||
@@ -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,
|
|
||||||
)
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
version = "1.2.6"
|
|
||||||
revision = 1
|
|
||||||
metadata = meta(
|
|
||||||
description = "Small, standards-conformant implementation of libc",
|
|
||||||
license = "MIT",
|
|
||||||
)
|
|
||||||
source = tarball_source(
|
|
||||||
url = f"https://musl.libc.org/releases/musl-{version}.tar.gz",
|
|
||||||
sha256 = "d585fd3b613c66151fc3249e8ed44f77020cb5e6c1e635a616d3f9f82460512a",
|
|
||||||
strip_components = 1,
|
|
||||||
)
|
|
||||||
host_deps = ["binutils", "gcc-bootstrap"]
|
|
||||||
|
|
||||||
def configure(ctx):
|
|
||||||
ctx.run([
|
|
||||||
ctx.source_dir + "/configure",
|
|
||||||
"--target=" + options.target_triple,
|
|
||||||
"--prefix=" + ctx.prefix,
|
|
||||||
"--syslibdir=/lib",
|
|
||||||
], env = {
|
|
||||||
"CC": options.target_triple + "-gcc",
|
|
||||||
"CFLAGS": options.cflags,
|
|
||||||
"LDFLAGS": options.ldflags,
|
|
||||||
})
|
|
||||||
|
|
||||||
_, build, install = autotools()
|
|
||||||
-905
@@ -1,905 +0,0 @@
|
|||||||
use std::{
|
|
||||||
cell::RefCell,
|
|
||||||
collections::{BTreeSet, VecDeque},
|
|
||||||
fs,
|
|
||||||
io::Write,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::{Command, Stdio},
|
|
||||||
rc::Rc,
|
|
||||||
thread,
|
|
||||||
time::{SystemTime, UNIX_EPOCH},
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{Context, bail};
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use starlark::values::OwnedFrozenValue;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
config::Config,
|
|
||||||
container::{Container, Mount},
|
|
||||||
graph::{TaskId, TaskPlan, TaskPlanner, task_recipe_slug},
|
|
||||||
layout::Layout,
|
|
||||||
log,
|
|
||||||
phase::{
|
|
||||||
self, PackageContext, PhaseArg, PhaseContext, PhaseRuntime, PhaseRuntimeGuard, SourceDir,
|
|
||||||
},
|
|
||||||
recipe::{GitSource, OutputPackage, Recipe, RecipeKind, RecipeSet, Source},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Builder {
|
|
||||||
root: PathBuf,
|
|
||||||
config: Config,
|
|
||||||
container_ready: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Builder {
|
|
||||||
pub fn new(root: PathBuf, config: Config) -> Self {
|
|
||||||
Self {
|
|
||||||
root,
|
|
||||||
config,
|
|
||||||
container_ready: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(
|
|
||||||
&mut self,
|
|
||||||
recipes: &RecipeSet,
|
|
||||||
requested: &[String],
|
|
||||||
rebuild: bool,
|
|
||||||
dry_run: bool,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let plan = TaskPlanner::new(&self.root, &self.config.arch, recipes)
|
|
||||||
.build_plan(requested, rebuild)?;
|
|
||||||
self.print_plan(&plan);
|
|
||||||
if dry_run {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
self.execute_plan(recipes, &plan)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch(
|
|
||||||
&mut self,
|
|
||||||
recipes: &RecipeSet,
|
|
||||||
requested: &[String],
|
|
||||||
dry_run: bool,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let plan =
|
|
||||||
TaskPlanner::new(&self.root, &self.config.arch, recipes).fetch_plan(requested)?;
|
|
||||||
self.print_plan(&plan);
|
|
||||||
if dry_run {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
self.execute_plan(recipes, &plan)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn ensure_container_ready(&mut self) -> anyhow::Result<()> {
|
|
||||||
if !self.container_ready {
|
|
||||||
self.ensure_container_image(&self.abs_config_path(&self.config.container_dockerfile))?;
|
|
||||||
self.container_ready = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn execute_plan(&mut self, recipes: &RecipeSet, plan: &TaskPlan) -> anyhow::Result<()> {
|
|
||||||
if plan.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut active: Option<ActiveContainer> = None;
|
|
||||||
|
|
||||||
for task in plan.order() {
|
|
||||||
let recipe_slug = task_recipe_slug(task, recipes)?;
|
|
||||||
|
|
||||||
if let Some(current) = active.as_ref() {
|
|
||||||
if current.recipe_key != recipe_slug {
|
|
||||||
active = None;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if task_needs_container(task) && active.is_none() {
|
|
||||||
self.ensure_container_ready()?;
|
|
||||||
active = Some(self.start_recipe_container(recipes, &recipe_slug)?);
|
|
||||||
}
|
|
||||||
|
|
||||||
let layout = Layout::new(&self.root, &self.config.arch);
|
|
||||||
self.run_task(&layout, recipes, task, active.as_ref())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_task(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipes: &RecipeSet,
|
|
||||||
task: &TaskId,
|
|
||||||
active: Option<&ActiveContainer>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
match task {
|
|
||||||
TaskId::FetchSources(key) => {
|
|
||||||
let recipe = recipes.recipe(key)?;
|
|
||||||
self.task_fetch_sources(layout, recipe)
|
|
||||||
}
|
|
||||||
TaskId::PrepareSources(key) => {
|
|
||||||
let recipe = recipes.recipe(key)?;
|
|
||||||
self.task_prepare_sources(layout, recipe)
|
|
||||||
}
|
|
||||||
TaskId::ConfigureRecipe(key) => {
|
|
||||||
let recipe = recipes.recipe(key)?;
|
|
||||||
let active = active.expect("configure task requires an active container");
|
|
||||||
self.task_configure(layout, recipe, active)
|
|
||||||
}
|
|
||||||
TaskId::BuildRecipe(key) => {
|
|
||||||
let recipe = recipes.recipe(key)?;
|
|
||||||
let active = active.expect("build task requires an active container");
|
|
||||||
self.task_build(layout, recipe, active)
|
|
||||||
}
|
|
||||||
TaskId::InstallPackageFiles(output_key) => {
|
|
||||||
let output = recipes.output(output_key)?;
|
|
||||||
let recipe = recipes.recipe(output.recipe())?;
|
|
||||||
let active = active.expect("install task requires an active container");
|
|
||||||
self.task_install_package(layout, recipe, output, active)
|
|
||||||
}
|
|
||||||
TaskId::ProduceApk(output_key) => {
|
|
||||||
let output = recipes.output(output_key)?;
|
|
||||||
let recipe = recipes.recipe(output.recipe())?;
|
|
||||||
let active = active.expect("apk task requires an active container");
|
|
||||||
self.task_produce_apk(layout, recipe, output, active)
|
|
||||||
}
|
|
||||||
TaskId::InstallHostRecipe(key) => {
|
|
||||||
let recipe = recipes.recipe(key)?;
|
|
||||||
let active = active.expect("host install task requires an active container");
|
|
||||||
self.task_install_host(layout, recipe, active)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_fetch_sources(&self, layout: &Layout<'_>, recipe: &Recipe) -> anyhow::Result<()> {
|
|
||||||
fs::create_dir_all(layout.source_cache_dir())?;
|
|
||||||
for (name, source) in recipe.sources().entries() {
|
|
||||||
let label = name.unwrap_or("source");
|
|
||||||
if !source.is_unknown_cache_key()
|
|
||||||
&& layout.source_cache_path(source.cache_key()).exists()
|
|
||||||
{
|
|
||||||
log::skip("fetch", &format!("{} ({})", recipe.key(), label));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
log::step(
|
|
||||||
"fetch",
|
|
||||||
&format!("{} ({}) from {}", recipe.key(), label, source.url()),
|
|
||||||
);
|
|
||||||
match source {
|
|
||||||
Source::Tarball(_) => self.fetch_tarball(layout, recipe, source)?,
|
|
||||||
Source::Git(git) => self.fetch_git(layout, recipe, git)?,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_tarball(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipe: &Recipe,
|
|
||||||
source: &Source,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let url = source.url();
|
|
||||||
let expected = source.cache_key().to_owned();
|
|
||||||
let unknown = source.is_unknown_cache_key();
|
|
||||||
|
|
||||||
let client = reqwest::blocking::Client::builder()
|
|
||||||
.user_agent("distro-builder")
|
|
||||||
.build()
|
|
||||||
.context("building HTTP client")?;
|
|
||||||
let mut response = client
|
|
||||||
.get(url)
|
|
||||||
.send()
|
|
||||||
.with_context(|| format!("downloading {url}"))?
|
|
||||||
.error_for_status()
|
|
||||||
.with_context(|| format!("downloading {url}"))?;
|
|
||||||
|
|
||||||
let cache_dir = layout.source_cache_dir();
|
|
||||||
fs::create_dir_all(&cache_dir)?;
|
|
||||||
let mut tmp = tempfile::NamedTempFile::new_in(&cache_dir)
|
|
||||||
.with_context(|| format!("creating temp file in {}", cache_dir.display()))?;
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
let mut buf = [0u8; 64 * 1024];
|
|
||||||
loop {
|
|
||||||
let n = std::io::Read::read(&mut response, &mut buf)
|
|
||||||
.with_context(|| format!("reading response body for {url}"))?;
|
|
||||||
if n == 0 {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
hasher.update(&buf[..n]);
|
|
||||||
tmp.write_all(&buf[..n])?;
|
|
||||||
}
|
|
||||||
tmp.flush()?;
|
|
||||||
let hash = hex::encode(hasher.finalize());
|
|
||||||
|
|
||||||
if unknown {
|
|
||||||
log::info(
|
|
||||||
"fetch",
|
|
||||||
&format!("{}: computed sha256 = {hash}", recipe.key()),
|
|
||||||
);
|
|
||||||
} else if hash != expected {
|
|
||||||
bail!("sha256 mismatch for {url}: expected {expected}, got {hash}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let final_path = layout.source_cache_path(&hash);
|
|
||||||
if !final_path.exists() {
|
|
||||||
tmp.persist(&final_path).with_context(|| {
|
|
||||||
format!("renaming downloaded archive to {}", final_path.display())
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if unknown {
|
|
||||||
bail!(
|
|
||||||
"{}: source sha256 is unknown; update the recipe with sha256 = \"{hash}\"",
|
|
||||||
recipe.key()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_git(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipe: &Recipe,
|
|
||||||
source: &GitSource,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let url = source.url();
|
|
||||||
let commit = source.commit();
|
|
||||||
let unknown = matches!(commit, "?" | "???");
|
|
||||||
|
|
||||||
let cache_dir = layout.source_cache_dir();
|
|
||||||
fs::create_dir_all(&cache_dir)?;
|
|
||||||
let tmp = tempfile::tempdir_in(&cache_dir)?;
|
|
||||||
let work = tmp.path().join("repo");
|
|
||||||
|
|
||||||
let status = Command::new("git")
|
|
||||||
.arg("clone")
|
|
||||||
.arg("--bare")
|
|
||||||
.arg(url)
|
|
||||||
.arg(&work)
|
|
||||||
.status()
|
|
||||||
.with_context(|| format!("spawning git clone {url}"))?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("git clone failed for {url} with {status}");
|
|
||||||
}
|
|
||||||
|
|
||||||
let rev = if unknown {
|
|
||||||
let out = Command::new("git")
|
|
||||||
.arg("-C")
|
|
||||||
.arg(&work)
|
|
||||||
.arg("rev-parse")
|
|
||||||
.arg("HEAD")
|
|
||||||
.output()
|
|
||||||
.context("spawning git rev-parse HEAD")?;
|
|
||||||
if !out.status.success() {
|
|
||||||
bail!("git rev-parse HEAD failed with {}", out.status);
|
|
||||||
}
|
|
||||||
let rev = String::from_utf8(out.stdout)?.trim().to_owned();
|
|
||||||
log::info(
|
|
||||||
"fetch",
|
|
||||||
&format!("{}: resolved git HEAD = {rev}", recipe.key()),
|
|
||||||
);
|
|
||||||
rev
|
|
||||||
} else {
|
|
||||||
let status = Command::new("git")
|
|
||||||
.arg("-C")
|
|
||||||
.arg(&work)
|
|
||||||
.arg("cat-file")
|
|
||||||
.arg("-e")
|
|
||||||
.arg(commit)
|
|
||||||
.status()
|
|
||||||
.context("spawning git cat-file")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("commit {commit} not found in {url}");
|
|
||||||
}
|
|
||||||
commit.to_owned()
|
|
||||||
};
|
|
||||||
|
|
||||||
let final_path = layout.source_cache_path(&rev);
|
|
||||||
if !final_path.exists() {
|
|
||||||
fs::rename(&work, &final_path).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"moving git clone to {} from {}",
|
|
||||||
final_path.display(),
|
|
||||||
work.display()
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if unknown {
|
|
||||||
bail!(
|
|
||||||
"{}: source commit is unknown; update the recipe with commit = \"{rev}\"",
|
|
||||||
recipe.key()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_prepare_sources(&self, layout: &Layout<'_>, recipe: &Recipe) -> anyhow::Result<()> {
|
|
||||||
log::step("prepare", &recipe.key());
|
|
||||||
let src = layout.source_workdir(recipe);
|
|
||||||
if src.exists() {
|
|
||||||
fs::remove_dir_all(&src).with_context(|| format!("clearing {}", src.display()))?;
|
|
||||||
}
|
|
||||||
fs::create_dir_all(&src)?;
|
|
||||||
let build = layout.build_workdir(recipe);
|
|
||||||
if build.exists() {
|
|
||||||
fs::remove_dir_all(&build).with_context(|| format!("clearing {}", build.display()))?;
|
|
||||||
}
|
|
||||||
fs::create_dir_all(&build)?;
|
|
||||||
|
|
||||||
for (name, source) in recipe.sources().entries() {
|
|
||||||
if source.is_unknown_cache_key() {
|
|
||||||
bail!(
|
|
||||||
"source for {} has an unknown cache key; run `distro fetch` first",
|
|
||||||
recipe.key()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let cache_path = layout.source_cache_path(source.cache_key());
|
|
||||||
if !cache_path.exists() {
|
|
||||||
bail!(
|
|
||||||
"missing cached source for {} at {}",
|
|
||||||
recipe.key(),
|
|
||||||
cache_path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let dst = match name {
|
|
||||||
None => src.clone(),
|
|
||||||
Some(named) => {
|
|
||||||
let dst = src.join(named);
|
|
||||||
fs::create_dir_all(&dst)?;
|
|
||||||
dst
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match source {
|
|
||||||
Source::Tarball(tar) => {
|
|
||||||
let mut cmd = Command::new("tar");
|
|
||||||
cmd.arg("-xf").arg(&cache_path);
|
|
||||||
if tar.strip_components() > 0 {
|
|
||||||
cmd.arg(format!("--strip-components={}", tar.strip_components()));
|
|
||||||
}
|
|
||||||
cmd.arg("-C").arg(&dst);
|
|
||||||
let status = cmd
|
|
||||||
.status()
|
|
||||||
.with_context(|| format!("spawning tar -xf {}", cache_path.display()))?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!(
|
|
||||||
"tar extraction of {} failed with {status}",
|
|
||||||
cache_path.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Source::Git(git) => {
|
|
||||||
let status = Command::new("git")
|
|
||||||
.arg("clone")
|
|
||||||
.arg(&cache_path)
|
|
||||||
.arg(&dst)
|
|
||||||
.status()
|
|
||||||
.context("spawning git clone from cache")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("git clone from cache failed with {status}");
|
|
||||||
}
|
|
||||||
let status = Command::new("git")
|
|
||||||
.arg("-C")
|
|
||||||
.arg(&dst)
|
|
||||||
.arg("checkout")
|
|
||||||
.arg(git.commit())
|
|
||||||
.status()
|
|
||||||
.context("spawning git checkout")?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("git checkout {} failed with {status}", git.commit());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let version_stamp = layout.source_stamp(recipe, "version");
|
|
||||||
fs::create_dir_all(version_stamp.parent().unwrap())?;
|
|
||||||
fs::write(
|
|
||||||
&version_stamp,
|
|
||||||
format!("{}-r{}", recipe.version(), recipe.revision()),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
if layout.recipe_has_patches(recipe)? {
|
|
||||||
log::skip(
|
|
||||||
"patches",
|
|
||||||
&format!("{}: patch application not implemented", recipe.key()),
|
|
||||||
);
|
|
||||||
fs::write(layout.source_stamp(recipe, "patched"), b"skipped")?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_configure(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipe: &Recipe,
|
|
||||||
active: &ActiveContainer,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
log::step("configure", &recipe.key());
|
|
||||||
if let Some(func) = recipe.phases().configure() {
|
|
||||||
let ctx = PhaseContext::new(
|
|
||||||
source_dir_for(recipe),
|
|
||||||
prefix_for(recipe.kind()),
|
|
||||||
default_jobs(),
|
|
||||||
);
|
|
||||||
self.invoke_with_runtime(active, &[PhaseArg::Ctx(ctx)], func)?;
|
|
||||||
}
|
|
||||||
self.write_recipe_stamp(layout, recipe, "configure")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_build(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipe: &Recipe,
|
|
||||||
active: &ActiveContainer,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
log::step("build", &recipe.key());
|
|
||||||
let ctx = PhaseContext::new(
|
|
||||||
source_dir_for(recipe),
|
|
||||||
prefix_for(recipe.kind()),
|
|
||||||
default_jobs(),
|
|
||||||
);
|
|
||||||
self.invoke_with_runtime(active, &[PhaseArg::Ctx(ctx)], recipe.phases().build())?;
|
|
||||||
self.write_recipe_stamp(layout, recipe, "build")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_install_package(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipe: &Recipe,
|
|
||||||
output: &OutputPackage,
|
|
||||||
active: &ActiveContainer,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
log::step("install", &output.key());
|
|
||||||
let dest = format!("/dest/{}", output.name());
|
|
||||||
active.container.borrow().exec(
|
|
||||||
&["mkdir".to_owned(), "-p".to_owned(), dest.clone()],
|
|
||||||
&base_env(&active.base_path),
|
|
||||||
"/",
|
|
||||||
)?;
|
|
||||||
let ctx = PhaseContext::new(
|
|
||||||
source_dir_for(recipe),
|
|
||||||
prefix_for(recipe.kind()),
|
|
||||||
default_jobs(),
|
|
||||||
);
|
|
||||||
let pkg = PackageContext::new(dest);
|
|
||||||
self.invoke_with_runtime(
|
|
||||||
active,
|
|
||||||
&[PhaseArg::Ctx(ctx), PhaseArg::Pkg(pkg)],
|
|
||||||
recipe.phases().install(),
|
|
||||||
)?;
|
|
||||||
self.write_output_stamp(layout, recipe, output, "install")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_install_host(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipe: &Recipe,
|
|
||||||
active: &ActiveContainer,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
log::step("host-install", &recipe.key());
|
|
||||||
let dest = format!("/dest/{}", recipe.slug());
|
|
||||||
active.container.borrow().exec(
|
|
||||||
&["mkdir".to_owned(), "-p".to_owned(), dest.clone()],
|
|
||||||
&base_env(&active.base_path),
|
|
||||||
"/",
|
|
||||||
)?;
|
|
||||||
let ctx = PhaseContext::new(
|
|
||||||
source_dir_for(recipe),
|
|
||||||
prefix_for(recipe.kind()),
|
|
||||||
default_jobs(),
|
|
||||||
);
|
|
||||||
let pkg = PackageContext::new(dest.clone());
|
|
||||||
self.invoke_with_runtime(
|
|
||||||
active,
|
|
||||||
&[PhaseArg::Ctx(ctx), PhaseArg::Pkg(pkg)],
|
|
||||||
recipe.phases().install(),
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let host_root = layout.host_install_root(recipe);
|
|
||||||
if host_root.exists() {
|
|
||||||
fs::remove_dir_all(&host_root)
|
|
||||||
.with_context(|| format!("clearing {}", host_root.display()))?;
|
|
||||||
}
|
|
||||||
fs::create_dir_all(&host_root)?;
|
|
||||||
active
|
|
||||||
.container
|
|
||||||
.borrow()
|
|
||||||
.cp_out(&format!("{dest}/."), &host_root)?;
|
|
||||||
|
|
||||||
self.write_recipe_stamp(layout, recipe, "host-install")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_produce_apk(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipe: &Recipe,
|
|
||||||
output: &OutputPackage,
|
|
||||||
active: &ActiveContainer,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
log::step("apk", &output.key());
|
|
||||||
let files_dir = format!("/dest/{}", output.name());
|
|
||||||
let file_name = format!(
|
|
||||||
"{}-{}-r{}.apk",
|
|
||||||
output.name(),
|
|
||||||
recipe.version(),
|
|
||||||
recipe.revision()
|
|
||||||
);
|
|
||||||
let out_in_container = format!("/pkgs/{file_name}");
|
|
||||||
let version = format!("{}-r{}", recipe.version(), recipe.revision());
|
|
||||||
|
|
||||||
let mut argv: Vec<String> = vec![
|
|
||||||
"apk".to_owned(),
|
|
||||||
"mkpkg".to_owned(),
|
|
||||||
"--files".to_owned(),
|
|
||||||
files_dir,
|
|
||||||
"--output".to_owned(),
|
|
||||||
out_in_container,
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("name:{}", output.name()),
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("version:{version}"),
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("arch:{}", self.config.arch),
|
|
||||||
"--info".to_owned(),
|
|
||||||
format!("origin:{}", recipe.key()),
|
|
||||||
];
|
|
||||||
if let Some(desc) = output.metadata().description() {
|
|
||||||
argv.push("--info".to_owned());
|
|
||||||
argv.push(format!("description:{desc}"));
|
|
||||||
}
|
|
||||||
if let Some(license) = output.metadata().license() {
|
|
||||||
argv.push("--info".to_owned());
|
|
||||||
argv.push(format!("license:{license}"));
|
|
||||||
}
|
|
||||||
if let Some(url) = output.metadata().website() {
|
|
||||||
argv.push("--info".to_owned());
|
|
||||||
argv.push(format!("url:{url}"));
|
|
||||||
}
|
|
||||||
if let Some(packager) = output.metadata().maintainer() {
|
|
||||||
argv.push("--info".to_owned());
|
|
||||||
argv.push(format!("packager:{packager}"));
|
|
||||||
}
|
|
||||||
for dep in recipe.deps().iter().chain(recipe.run_deps().iter()) {
|
|
||||||
argv.push("--info".to_owned());
|
|
||||||
argv.push(format!("depends:{dep}"));
|
|
||||||
}
|
|
||||||
|
|
||||||
active
|
|
||||||
.container
|
|
||||||
.borrow()
|
|
||||||
.exec(&argv, &base_env(&active.base_path), "/")?;
|
|
||||||
|
|
||||||
self.write_output_stamp(layout, recipe, output, "apk")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn invoke_with_runtime(
|
|
||||||
&self,
|
|
||||||
active: &ActiveContainer,
|
|
||||||
args: &[PhaseArg],
|
|
||||||
func: &OwnedFrozenValue,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let runtime = PhaseRuntime {
|
|
||||||
container: active.container.clone(),
|
|
||||||
base_path: active.base_path.clone(),
|
|
||||||
base_env: bare_env(),
|
|
||||||
};
|
|
||||||
let _guard = PhaseRuntimeGuard::enter(runtime);
|
|
||||||
phase::invoke_phase(func, args)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_recipe_container(
|
|
||||||
&self,
|
|
||||||
recipes: &RecipeSet,
|
|
||||||
recipe_key: &str,
|
|
||||||
) -> anyhow::Result<ActiveContainer> {
|
|
||||||
let recipe = recipes.recipe(recipe_key)?;
|
|
||||||
let layout = Layout::new(&self.root, &self.config.arch);
|
|
||||||
|
|
||||||
let source_dir = layout.source_workdir(recipe);
|
|
||||||
let build_dir = layout.build_workdir(recipe);
|
|
||||||
fs::create_dir_all(&source_dir)?;
|
|
||||||
fs::create_dir_all(&build_dir)?;
|
|
||||||
|
|
||||||
let host_deps = transitive_host_deps(recipes, recipe)?;
|
|
||||||
let pkgs_dir = self.root.join("build/pkgs").join(&self.config.arch);
|
|
||||||
fs::create_dir_all(&pkgs_dir)?;
|
|
||||||
let mut mounts = vec![
|
|
||||||
Mount {
|
|
||||||
host: source_dir,
|
|
||||||
container: "/sources".to_owned(),
|
|
||||||
read_only: false,
|
|
||||||
},
|
|
||||||
Mount {
|
|
||||||
host: build_dir,
|
|
||||||
container: "/build".to_owned(),
|
|
||||||
read_only: false,
|
|
||||||
},
|
|
||||||
Mount {
|
|
||||||
host: pkgs_dir,
|
|
||||||
container: "/pkgs".to_owned(),
|
|
||||||
read_only: false,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
let mut tools_bins: Vec<String> = Vec::new();
|
|
||||||
for dep_key in &host_deps {
|
|
||||||
let dep_recipe = recipes.recipe(dep_key)?;
|
|
||||||
let install = layout.host_install_dir(dep_recipe);
|
|
||||||
if !install.exists() {
|
|
||||||
bail!(
|
|
||||||
"missing host install for {dep_key} at {}; build it first",
|
|
||||||
install.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let bare = dep_key.strip_prefix("host:").unwrap_or(dep_key);
|
|
||||||
mounts.push(Mount {
|
|
||||||
host: install,
|
|
||||||
container: format!("/tools/{bare}/usr/local"),
|
|
||||||
read_only: true,
|
|
||||||
});
|
|
||||||
tools_bins.push(format!("/tools/{bare}/usr/local/bin"));
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = format!(
|
|
||||||
"distro-builder-{}-{:x}",
|
|
||||||
std::process::id(),
|
|
||||||
random_suffix()
|
|
||||||
);
|
|
||||||
let container = Container::start(
|
|
||||||
&self.config.container_runtime,
|
|
||||||
&self.config.container_image,
|
|
||||||
&name,
|
|
||||||
&mounts,
|
|
||||||
)?;
|
|
||||||
|
|
||||||
let mut path_segments = tools_bins;
|
|
||||||
path_segments.push("/usr/local/sbin".to_owned());
|
|
||||||
path_segments.push("/usr/local/bin".to_owned());
|
|
||||||
path_segments.push("/usr/sbin".to_owned());
|
|
||||||
path_segments.push("/usr/bin".to_owned());
|
|
||||||
path_segments.push("/sbin".to_owned());
|
|
||||||
path_segments.push("/bin".to_owned());
|
|
||||||
let base_path = path_segments.join(":");
|
|
||||||
|
|
||||||
let active = ActiveContainer {
|
|
||||||
recipe_key: recipe_key.to_owned(),
|
|
||||||
container: Rc::new(RefCell::new(container)),
|
|
||||||
base_path,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(active)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_recipe_stamp(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipe: &Recipe,
|
|
||||||
kind: &str,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let path = layout.recipe_task_stamp(recipe, kind);
|
|
||||||
fs::create_dir_all(path.parent().unwrap())?;
|
|
||||||
fs::write(path, layout.recipe_fingerprint(recipe)?)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_output_stamp(
|
|
||||||
&self,
|
|
||||||
layout: &Layout<'_>,
|
|
||||||
recipe: &Recipe,
|
|
||||||
output: &OutputPackage,
|
|
||||||
kind: &str,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let path = layout.output_task_stamp(output, kind);
|
|
||||||
fs::create_dir_all(path.parent().unwrap())?;
|
|
||||||
fs::write(path, layout.output_fingerprint(recipe, output)?)?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn ensure_container_image(&self, dockerfile: &Path) -> anyhow::Result<()> {
|
|
||||||
if !dockerfile.exists() {
|
|
||||||
bail!(
|
|
||||||
"configured container Dockerfile does not exist: {}",
|
|
||||||
dockerfile.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let hash = self.container_build_hash(dockerfile)?;
|
|
||||||
let stamp = self.root.join("build/container-image.hash");
|
|
||||||
if fs::read_to_string(&stamp).ok().as_deref() == Some(hash.as_str())
|
|
||||||
&& self.container_image_exists()?
|
|
||||||
{
|
|
||||||
log::skip(
|
|
||||||
"image",
|
|
||||||
&format!("using cached {}", self.config.container_image),
|
|
||||||
);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
log::step(
|
|
||||||
"image",
|
|
||||||
&format!(
|
|
||||||
"building {} from {}",
|
|
||||||
self.config.container_image,
|
|
||||||
dockerfile.display()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
let runtime = self.config.container_runtime.as_str();
|
|
||||||
let status = Command::new(runtime)
|
|
||||||
.arg("build")
|
|
||||||
.arg("-f")
|
|
||||||
.arg(dockerfile)
|
|
||||||
.arg("-t")
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.arg(&self.root)
|
|
||||||
.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) -> anyhow::Result<bool> {
|
|
||||||
let runtime = self.config.container_runtime.as_str();
|
|
||||||
let status = Command::new(runtime)
|
|
||||||
.arg("image")
|
|
||||||
.arg("exists")
|
|
||||||
.arg(&self.config.container_image)
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.status()
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"failed to inspect container image `{}`",
|
|
||||||
self.config.container_image
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
Ok(status.success())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn container_build_hash(&self, dockerfile: &Path) -> anyhow::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 print_plan(&self, plan: &TaskPlan) {
|
|
||||||
if plan.is_empty() {
|
|
||||||
log::skip("plan", "nothing to do");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log::step(
|
|
||||||
"plan",
|
|
||||||
&format!(
|
|
||||||
"{} active task(s), {} edge(s)",
|
|
||||||
plan.order().len(),
|
|
||||||
plan.dependency_count()
|
|
||||||
),
|
|
||||||
);
|
|
||||||
for task in plan.order() {
|
|
||||||
println!("{task}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn abs_config_path(&self, path: &Path) -> PathBuf {
|
|
||||||
if path.is_absolute() {
|
|
||||||
path.to_path_buf()
|
|
||||||
} else {
|
|
||||||
self.root.join(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ActiveContainer {
|
|
||||||
recipe_key: String,
|
|
||||||
container: Rc<RefCell<Container>>,
|
|
||||||
base_path: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn task_needs_container(task: &TaskId) -> bool {
|
|
||||||
matches!(
|
|
||||||
task,
|
|
||||||
TaskId::ConfigureRecipe(_)
|
|
||||||
| TaskId::BuildRecipe(_)
|
|
||||||
| TaskId::InstallPackageFiles(_)
|
|
||||||
| TaskId::ProduceApk(_)
|
|
||||||
| TaskId::InstallHostRecipe(_)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prefix_for(kind: RecipeKind) -> &'static str {
|
|
||||||
match kind {
|
|
||||||
RecipeKind::Package => "/usr",
|
|
||||||
RecipeKind::HostPackage => "/usr/local",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source_dir_for(recipe: &Recipe) -> SourceDir {
|
|
||||||
let entries = recipe.sources().entries();
|
|
||||||
let named: Vec<(&str, &crate::recipe::Source)> = entries
|
|
||||||
.iter()
|
|
||||||
.filter_map(|(name, src)| name.map(|n| (n, *src)))
|
|
||||||
.collect();
|
|
||||||
if named.is_empty() {
|
|
||||||
SourceDir::single("/sources")
|
|
||||||
} else {
|
|
||||||
SourceDir::named(
|
|
||||||
named
|
|
||||||
.into_iter()
|
|
||||||
.map(|(n, _)| (n.to_owned(), format!("/sources/{n}"))),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_jobs() -> i32 {
|
|
||||||
thread::available_parallelism()
|
|
||||||
.map(|n| n.get() as i32)
|
|
||||||
.unwrap_or(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bare_env() -> Vec<(String, String)> {
|
|
||||||
vec![
|
|
||||||
("PATH".to_owned(), String::new()),
|
|
||||||
("HOME".to_owned(), "/tmp".to_owned()),
|
|
||||||
("TERM".to_owned(), "dumb".to_owned()),
|
|
||||||
("LC_ALL".to_owned(), "C".to_owned()),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn base_env(path: &str) -> Vec<(String, String)> {
|
|
||||||
let mut env = bare_env();
|
|
||||||
if let Some(slot) = env.iter_mut().find(|(k, _)| k == "PATH") {
|
|
||||||
slot.1 = path.to_owned();
|
|
||||||
}
|
|
||||||
env
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transitive_host_deps(recipes: &RecipeSet, recipe: &Recipe) -> anyhow::Result<Vec<String>> {
|
|
||||||
let mut order: Vec<String> = Vec::new();
|
|
||||||
let mut seen: BTreeSet<String> = BTreeSet::new();
|
|
||||||
let mut queue: VecDeque<String> = recipe
|
|
||||||
.host_deps()
|
|
||||||
.iter()
|
|
||||||
.map(|d| RecipeKind::HostPackage.key(d))
|
|
||||||
.collect();
|
|
||||||
while let Some(key) = queue.pop_front() {
|
|
||||||
if !seen.insert(key.clone()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let dep = recipes.recipe(&key)?;
|
|
||||||
for sub in dep.host_deps() {
|
|
||||||
queue.push_back(RecipeKind::HostPackage.key(sub));
|
|
||||||
}
|
|
||||||
order.push(key);
|
|
||||||
}
|
|
||||||
Ok(order)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn random_suffix() -> u64 {
|
|
||||||
let nanos = SystemTime::now()
|
|
||||||
.duration_since(UNIX_EPOCH)
|
|
||||||
.map(|d| d.subsec_nanos() as u64)
|
|
||||||
.unwrap_or(0);
|
|
||||||
nanos.wrapping_mul(0x9E3779B97F4A7C15)
|
|
||||||
}
|
|
||||||
+83
-23
@@ -1,13 +1,27 @@
|
|||||||
use clap::{Parser, Subcommand};
|
use crate::{
|
||||||
use std::path::PathBuf;
|
container::{ContainerManager, PodmanRuntime},
|
||||||
|
eval::{
|
||||||
|
Config, ContainerConfig, ContainerManagerWrapper, Context, Path, config_globals,
|
||||||
|
eval_files, types_globals,
|
||||||
|
},
|
||||||
|
log,
|
||||||
|
plan::{Plan, PlanKey},
|
||||||
|
recipe::RecipeSet,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{builder::Builder, config::Config, eval, recipe::RecipeSet};
|
use clap::{Parser, Subcommand};
|
||||||
|
use starlark::{
|
||||||
|
environment::{GlobalsBuilder, Module},
|
||||||
|
eval,
|
||||||
|
values::Value,
|
||||||
|
};
|
||||||
|
use std::{cell::Cell, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[arg(
|
#[arg(
|
||||||
long,
|
long,
|
||||||
short,
|
short = 'C',
|
||||||
default_value = ".",
|
default_value = ".",
|
||||||
help = "Directory containing the configuration and recipe files"
|
help = "Directory containing the configuration and recipe files"
|
||||||
)]
|
)]
|
||||||
@@ -46,32 +60,78 @@ struct BuildCommand {
|
|||||||
enum Command {
|
enum Command {
|
||||||
Fetch(FetchCommand),
|
Fetch(FetchCommand),
|
||||||
Build(BuildCommand),
|
Build(BuildCommand),
|
||||||
#[command(about = "Create or refresh the configured build container image")]
|
|
||||||
Image,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run() -> anyhow::Result<()> {
|
pub fn run() -> anyhow::Result<()> {
|
||||||
let cli = Cli::parse();
|
let cli = Cli::parse();
|
||||||
|
|
||||||
let root_path = cli.root.canonicalize().unwrap_or(cli.root);
|
let root_path = cli.root.canonicalize().unwrap_or(cli.root);
|
||||||
let config = Config::load(&root_path.join("config.star"))?;
|
|
||||||
|
let config: Config = {
|
||||||
|
let cell = Cell::new(None);
|
||||||
|
let config_path = root_path.join("config.star");
|
||||||
|
|
||||||
|
eval_files(
|
||||||
|
&[&config_path],
|
||||||
|
&GlobalsBuilder::standard()
|
||||||
|
.with(types_globals)
|
||||||
|
.with(config_globals)
|
||||||
|
.build(),
|
||||||
|
None,
|
||||||
|
None,
|
||||||
|
Some(&cell),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
cell.take()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("`config` was not called"))?
|
||||||
|
};
|
||||||
|
|
||||||
|
let container_runtime = match config.container() {
|
||||||
|
ContainerConfig::Podman(_) => Arc::new(PodmanRuntime::new()?),
|
||||||
|
};
|
||||||
|
|
||||||
|
let container_manager = ContainerManager::new(container_runtime);
|
||||||
|
let mut recipes = RecipeSet::new(&config);
|
||||||
|
|
||||||
|
recipes.load_recipes(
|
||||||
|
&root_path.join(config.recipes_dir()),
|
||||||
|
&root_path.join(config.host_recipes_dir()),
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// let wrapper = ContainerManagerWrapper(&container_manager);
|
||||||
|
// for (name, recipe) in recipes.packages.iter() {
|
||||||
|
// println!("{name}: {:#?}", recipe);
|
||||||
|
|
||||||
|
// let mo = Module::new();
|
||||||
|
// let mut eval = eval::Evaluator::new(&mo);
|
||||||
|
// eval.extra = Some(&wrapper);
|
||||||
|
// eval.eval_function(
|
||||||
|
// recipe.build.unwrap().0.to_value(),
|
||||||
|
// &[mo.heap().alloc(Context {
|
||||||
|
// source_dir: Path::new("/source"),
|
||||||
|
// build_dir: Path::new("/build"),
|
||||||
|
// jobs: 4,
|
||||||
|
// })],
|
||||||
|
// &[],
|
||||||
|
// )
|
||||||
|
// .unwrap();
|
||||||
|
// }
|
||||||
|
|
||||||
|
let mut plan = Plan::new(&recipes);
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Fetch(command) => {
|
Command::Fetch(_) => {}
|
||||||
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
|
Command::Build(cmd) => {
|
||||||
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
|
for recipe in cmd.recipes.iter() {
|
||||||
let mut builder = Builder::new(root_path, config);
|
plan.add_wanted(if let Some(recipe) = recipe.strip_prefix("host:") {
|
||||||
builder.fetch(&recipes, &command.recipes, command.dry_run)
|
PlanKey::ToolInstall(recipe.to_string())
|
||||||
}
|
} else {
|
||||||
Command::Build(command) => {
|
PlanKey::PkgPackage(recipe.clone())
|
||||||
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
|
});
|
||||||
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
|
|
||||||
let mut builder = Builder::new(root_path, config);
|
|
||||||
builder.build(&recipes, &command.recipes, command.rebuild, command.dry_run)
|
|
||||||
}
|
|
||||||
Command::Image => {
|
|
||||||
let mut builder = Builder::new(root_path, config);
|
|
||||||
builder.ensure_container_ready()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log!("plan", "{:#?}", plan.steps());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|||||||
-117
@@ -1,117 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use starlark::values::dict::FrozenDictRef;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
eval::{ExtractError, eval_file, extract_string},
|
|
||||||
options::Options,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ContainerRuntime {
|
|
||||||
Docker,
|
|
||||||
Podman,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContainerRuntime {
|
|
||||||
pub fn as_str(&self) -> &'static str {
|
|
||||||
match self {
|
|
||||||
Self::Docker => "docker",
|
|
||||||
Self::Podman => "podman",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ContainerRuntime {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(self.as_str())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for ContainerRuntime {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
|
|
||||||
fn try_from(value: &str) -> anyhow::Result<Self> {
|
|
||||||
match value {
|
|
||||||
"docker" => Ok(Self::Docker),
|
|
||||||
"podman" => Ok(Self::Podman),
|
|
||||||
_ => anyhow::bail!("invalid runtime: {value}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Config {
|
|
||||||
pub container_runtime: ContainerRuntime,
|
|
||||||
pub container_image: String,
|
|
||||||
pub container_dockerfile: PathBuf,
|
|
||||||
|
|
||||||
pub arch: String,
|
|
||||||
pub options: Options,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Config {
|
|
||||||
pub fn load(path: &Path) -> anyhow::Result<Self> {
|
|
||||||
let module = eval_file(path, None, None)?;
|
|
||||||
|
|
||||||
let container_runtime = match extract_string(&module, "container_runtime") {
|
|
||||||
Ok(v) => ContainerRuntime::try_from(v.as_str())?,
|
|
||||||
Err(ExtractError::NotFound) => ContainerRuntime::Podman,
|
|
||||||
Err(ExtractError::TypeMismatch) => anyhow::bail!("`container_runtime` is not a string"),
|
|
||||||
};
|
|
||||||
let container_image = match extract_string(&module, "container_image") {
|
|
||||||
Ok(container_image) => container_image,
|
|
||||||
Err(ExtractError::NotFound) => {
|
|
||||||
anyhow::bail!("`container_image` config variable not set")
|
|
||||||
}
|
|
||||||
Err(ExtractError::TypeMismatch) => anyhow::bail!("`container_image` is not a string"),
|
|
||||||
};
|
|
||||||
let container_dockerfile = match extract_string(&module, "container_dockerfile") {
|
|
||||||
Ok(container_dockerfile) => PathBuf::from(container_dockerfile),
|
|
||||||
Err(ExtractError::NotFound) => PathBuf::from("Dockerfile"),
|
|
||||||
Err(ExtractError::TypeMismatch) => {
|
|
||||||
anyhow::bail!("`container_dockerfile` is not a string")
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let arch = match extract_string(&module, "arch") {
|
|
||||||
Ok(arch) => arch,
|
|
||||||
Err(ExtractError::NotFound) => anyhow::bail!("`arch` config variable not set"),
|
|
||||||
Err(ExtractError::TypeMismatch) => anyhow::bail!("`arch` is not a string"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let frozen_module = module.freeze()?;
|
|
||||||
let options_value = frozen_module
|
|
||||||
.get_option("options")?
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("`options` config variable not set"))?;
|
|
||||||
let entries = {
|
|
||||||
// SAFETY: the FrozenValue is only used to construct a FrozenDictRef whose
|
|
||||||
// lifetime is bounded by `options_value`, which keeps the frozen heap alive.
|
|
||||||
let dict =
|
|
||||||
FrozenDictRef::from_frozen_value(unsafe { options_value.unchecked_frozen_value() })
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("`options` is not a dict"))?;
|
|
||||||
dict.iter()
|
|
||||||
.map(|(k, v)| {
|
|
||||||
let key = k
|
|
||||||
.to_value()
|
|
||||||
.unpack_str()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("non-string key in `options`"))?
|
|
||||||
.to_owned();
|
|
||||||
Ok((key, options_value.map(|_| v)))
|
|
||||||
})
|
|
||||||
.collect::<anyhow::Result<HashMap<_, _>>>()?
|
|
||||||
};
|
|
||||||
let options = Options::new(entries);
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
container_runtime,
|
|
||||||
container_image,
|
|
||||||
container_dockerfile,
|
|
||||||
|
|
||||||
arch,
|
|
||||||
options,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
use std::{
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::{Command, Stdio},
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::{Context, bail};
|
|
||||||
|
|
||||||
use crate::config::ContainerRuntime;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct Mount {
|
|
||||||
pub host: PathBuf,
|
|
||||||
pub container: String,
|
|
||||||
pub read_only: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Container {
|
|
||||||
runtime: &'static str,
|
|
||||||
id: String,
|
|
||||||
stopped: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Container {
|
|
||||||
pub fn start(
|
|
||||||
runtime: &ContainerRuntime,
|
|
||||||
image: &str,
|
|
||||||
name: &str,
|
|
||||||
mounts: &[Mount],
|
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
let runtime_str = runtime.as_str();
|
|
||||||
let mut cmd = Command::new(runtime_str);
|
|
||||||
cmd.arg("run")
|
|
||||||
.arg("-d")
|
|
||||||
.arg("--rm")
|
|
||||||
.arg("--name")
|
|
||||||
.arg(name)
|
|
||||||
.arg("--read-only")
|
|
||||||
.arg("--tmpfs")
|
|
||||||
.arg("/tmp")
|
|
||||||
.arg("--tmpfs")
|
|
||||||
.arg("/dest")
|
|
||||||
.arg("--tmpfs")
|
|
||||||
.arg("/sysroot")
|
|
||||||
.arg("--network=none");
|
|
||||||
|
|
||||||
if matches!(runtime, ContainerRuntime::Podman) {
|
|
||||||
cmd.arg("--userns=keep-id");
|
|
||||||
}
|
|
||||||
|
|
||||||
for mount in mounts {
|
|
||||||
let mut spec = format!("{}:{}", mount.host.display(), mount.container);
|
|
||||||
if mount.read_only {
|
|
||||||
spec.push_str(":ro");
|
|
||||||
}
|
|
||||||
cmd.arg("-v").arg(spec);
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.arg(image).arg("sleep").arg("infinity");
|
|
||||||
cmd.stdout(Stdio::piped()).stderr(Stdio::inherit());
|
|
||||||
|
|
||||||
let output = cmd
|
|
||||||
.output()
|
|
||||||
.with_context(|| format!("spawning `{runtime_str} run` for image `{image}`"))?;
|
|
||||||
if !output.status.success() {
|
|
||||||
bail!(
|
|
||||||
"`{runtime_str} run` failed with {} for image `{image}`",
|
|
||||||
output.status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let id = String::from_utf8(output.stdout)
|
|
||||||
.context("container id is not valid UTF-8")?
|
|
||||||
.trim()
|
|
||||||
.to_owned();
|
|
||||||
if id.is_empty() {
|
|
||||||
bail!("`{runtime_str} run` returned an empty container id");
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
|
||||||
runtime: runtime_str,
|
|
||||||
id,
|
|
||||||
stopped: false,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn exec(&self, argv: &[String], env: &[(String, String)], cwd: &str) -> anyhow::Result<()> {
|
|
||||||
if argv.is_empty() {
|
|
||||||
bail!("ctx.run called with an empty argv");
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut cmd = Command::new(self.runtime);
|
|
||||||
cmd.arg("exec").arg("-w").arg(cwd);
|
|
||||||
for (k, v) in env {
|
|
||||||
cmd.arg("-e").arg(format!("{k}={v}"));
|
|
||||||
}
|
|
||||||
cmd.arg(&self.id);
|
|
||||||
cmd.args(argv);
|
|
||||||
|
|
||||||
let status = cmd.status().with_context(|| {
|
|
||||||
format!("spawning `{} exec` in container {}", self.runtime, self.id)
|
|
||||||
})?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("command {argv:?} failed with {status}");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cp_out(&self, src_in_container: &str, host_dst: &Path) -> anyhow::Result<()> {
|
|
||||||
let spec = format!("{}:{}", self.id, src_in_container);
|
|
||||||
let status = Command::new(self.runtime)
|
|
||||||
.arg("cp")
|
|
||||||
.arg(spec)
|
|
||||||
.arg(host_dst)
|
|
||||||
.status()
|
|
||||||
.with_context(|| format!("spawning `{} cp`", self.runtime))?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!(
|
|
||||||
"`{} cp` failed with {status} for {src_in_container} -> {}",
|
|
||||||
self.runtime,
|
|
||||||
host_dst.display()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn stop(mut self) -> anyhow::Result<()> {
|
|
||||||
self.stop_inner()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn stop_inner(&mut self) -> anyhow::Result<()> {
|
|
||||||
if self.stopped {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
self.stopped = true;
|
|
||||||
let status = Command::new(self.runtime)
|
|
||||||
.arg("rm")
|
|
||||||
.arg("-f")
|
|
||||||
.arg(&self.id)
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.status()
|
|
||||||
.with_context(|| format!("spawning `{} rm`", self.runtime))?;
|
|
||||||
if !status.success() {
|
|
||||||
bail!("`{} rm -f {}` failed with {status}", self.runtime, self.id);
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for Container {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
if !self.stopped {
|
|
||||||
let _ = Command::new(self.runtime)
|
|
||||||
.arg("rm")
|
|
||||||
.arg("-f")
|
|
||||||
.arg(&self.id)
|
|
||||||
.stdout(Stdio::null())
|
|
||||||
.stderr(Stdio::null())
|
|
||||||
.status();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::Path,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
|
};
|
||||||
|
|
||||||
|
mod podman;
|
||||||
|
|
||||||
|
pub use podman::PodmanRuntime;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Container {
|
||||||
|
id: String,
|
||||||
|
runtime: Arc<dyn ContainerRuntime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Container {
|
||||||
|
fn new(id: String, runtime: Arc<dyn ContainerRuntime>) -> Self {
|
||||||
|
Self { id, runtime }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn id(&self) -> &str {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn exec<'a, 'e, 'c>(
|
||||||
|
&self,
|
||||||
|
argv: impl Into<Vec<&'a str>>,
|
||||||
|
env: impl Into<Vec<(&'e str, &'e str)>>,
|
||||||
|
cwd: impl Into<&'c Path>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
self.runtime
|
||||||
|
.exec(self.id(), argv.into(), env.into(), cwd.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ContainerRuntime {
|
||||||
|
/// Starts a new container, returns the ID.
|
||||||
|
fn start_container(
|
||||||
|
&self,
|
||||||
|
image_name: &str,
|
||||||
|
mounts: &[(&Path, &str, bool)],
|
||||||
|
) -> anyhow::Result<String>;
|
||||||
|
|
||||||
|
/// Stops a container.
|
||||||
|
fn stop_container(&self, container_id: &str);
|
||||||
|
|
||||||
|
/// Executes a command in a container.
|
||||||
|
fn exec(
|
||||||
|
&self,
|
||||||
|
container_id: &str,
|
||||||
|
argv: Vec<&str>,
|
||||||
|
env: Vec<(&str, &str)>,
|
||||||
|
cwd: &Path,
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ContainerManager {
|
||||||
|
inner: Mutex<ContainerManagerInner>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContainerManagerInner {
|
||||||
|
containers: HashMap<String, Container>,
|
||||||
|
runtime: Arc<dyn ContainerRuntime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerManager {
|
||||||
|
pub fn new(runtime: Arc<dyn ContainerRuntime>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Mutex::new(ContainerManagerInner {
|
||||||
|
containers: HashMap::new(),
|
||||||
|
runtime,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn container(&self, name: &str) -> anyhow::Result<Container> {
|
||||||
|
let mut inner = self.inner.lock().unwrap();
|
||||||
|
if inner.containers.get(name).is_none() {
|
||||||
|
let container_id = inner.runtime.start_container("alpine:edge", &[])?;
|
||||||
|
|
||||||
|
crate::log!("info", "Started new container ({container_id})");
|
||||||
|
|
||||||
|
let container = Container::new(container_id, inner.runtime.clone());
|
||||||
|
inner.containers.insert(name.into(), container);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(inner.containers.get(name).cloned().unwrap())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ContainerManager {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
let inner = self.inner.lock().unwrap();
|
||||||
|
for (_, container) in inner.containers.iter() {
|
||||||
|
inner.runtime.stop_container(container.id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
use crate::container::ContainerRuntime;
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use std::{
|
||||||
|
path::Path,
|
||||||
|
process::{Command, Stdio},
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct PodmanRuntime;
|
||||||
|
|
||||||
|
impl PodmanRuntime {
|
||||||
|
pub fn new() -> anyhow::Result<Self> {
|
||||||
|
let output = Command::new("podman").arg("--version").output()?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(Self)
|
||||||
|
} else {
|
||||||
|
anyhow::bail!(
|
||||||
|
"Could not execute `podman --version` - make sure you have podman installed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContainerRuntime for PodmanRuntime {
|
||||||
|
fn start_container(
|
||||||
|
&self,
|
||||||
|
image_name: &str,
|
||||||
|
mounts: &[(&Path, &str, bool)],
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let mut cmd = Command::new("podman");
|
||||||
|
|
||||||
|
cmd.arg("run");
|
||||||
|
cmd.arg("--detach");
|
||||||
|
cmd.arg("--read-only");
|
||||||
|
cmd.arg("--network=none");
|
||||||
|
cmd.arg("--userns=keep-id");
|
||||||
|
cmd.arg("--tmpfs").arg("/builder/dest");
|
||||||
|
cmd.arg("--tmpfs").arg("/builder/sysroot");
|
||||||
|
|
||||||
|
for &(host, container, read_only) in mounts {
|
||||||
|
cmd.arg("--volume").arg(format!(
|
||||||
|
"{}:{}{}",
|
||||||
|
host.display(),
|
||||||
|
container,
|
||||||
|
if read_only { ":ro" } else { "" }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.arg(image_name);
|
||||||
|
cmd.arg("sleep").arg("infinity");
|
||||||
|
|
||||||
|
let output = cmd.output()?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(String::from_utf8(output.stdout)
|
||||||
|
.context("container ID is not valid UTF-8")?
|
||||||
|
.trim()
|
||||||
|
.into())
|
||||||
|
} else {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_container(&self, container_id: &str) {
|
||||||
|
Command::new("podman")
|
||||||
|
.arg("kill")
|
||||||
|
.arg(container_id)
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exec(
|
||||||
|
&self,
|
||||||
|
container_id: &str,
|
||||||
|
argv: Vec<&str>,
|
||||||
|
env: Vec<(&str, &str)>,
|
||||||
|
cwd: &Path,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let mut cmd = Command::new("podman");
|
||||||
|
|
||||||
|
cmd.arg("exec");
|
||||||
|
cmd.arg("--workdir").arg(cwd);
|
||||||
|
|
||||||
|
for (key, value) in env {
|
||||||
|
cmd.arg("--env").arg(format!("{key}={value}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.arg(container_id);
|
||||||
|
cmd.args(argv);
|
||||||
|
cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
|
||||||
|
|
||||||
|
let output = cmd.output()?;
|
||||||
|
|
||||||
|
if output.status.success() {
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("Failed to execute command");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
-177
@@ -1,177 +0,0 @@
|
|||||||
use anyhow::Context;
|
|
||||||
use starlark::{
|
|
||||||
environment::{FrozenModule, Globals, GlobalsBuilder, Module},
|
|
||||||
eval::Evaluator,
|
|
||||||
syntax::{AstModule, Dialect},
|
|
||||||
values::list::ListRef,
|
|
||||||
};
|
|
||||||
use std::path::{Path, PathBuf};
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
options::Options,
|
|
||||||
recipe::{GitSource, Metadata, Source, Subpackage, TarballSource},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum ExtractError {
|
|
||||||
NotFound,
|
|
||||||
TypeMismatch,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for ExtractError {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
match self {
|
|
||||||
ExtractError::NotFound => write!(f, "missing"),
|
|
||||||
ExtractError::TypeMismatch => write!(f, "wrong type"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::error::Error for ExtractError {}
|
|
||||||
|
|
||||||
#[starlark::starlark_module]
|
|
||||||
fn builder_globals(builder: &mut GlobalsBuilder) {
|
|
||||||
fn meta(
|
|
||||||
maintainer: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
license: Option<String>,
|
|
||||||
website: Option<String>,
|
|
||||||
) -> anyhow::Result<Metadata> {
|
|
||||||
Ok(Metadata::new(maintainer, description, license, website))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn tarball_source(
|
|
||||||
url: String,
|
|
||||||
sha256: String,
|
|
||||||
strip_components: Option<u32>,
|
|
||||||
) -> anyhow::Result<Source> {
|
|
||||||
Ok(Source::Tarball(TarballSource::new(
|
|
||||||
url,
|
|
||||||
sha256,
|
|
||||||
strip_components.unwrap_or(0),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn git_source(url: String, commit: String) -> anyhow::Result<Source> {
|
|
||||||
Ok(Source::Git(GitSource::new(url, commit)))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn subpackage(name: String, metadata: Option<&Metadata>) -> anyhow::Result<Subpackage> {
|
|
||||||
let metadata = metadata
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| Metadata::new(None, None, None, None));
|
|
||||||
Ok(Subpackage::new(name, metadata))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_file(
|
|
||||||
path: &Path,
|
|
||||||
options: Option<&Options>,
|
|
||||||
lib: Option<&FrozenModule>,
|
|
||||||
) -> anyhow::Result<Module> {
|
|
||||||
let module = Module::new();
|
|
||||||
if let Some(lib) = lib {
|
|
||||||
module.import_public_symbols(lib);
|
|
||||||
}
|
|
||||||
if let Some(options) = options {
|
|
||||||
inject_options(&module, options);
|
|
||||||
}
|
|
||||||
|
|
||||||
let ast = AstModule::parse_file(path, &dialect()).map_err(|err| anyhow::anyhow!("{err}"))?;
|
|
||||||
let globals = globals();
|
|
||||||
|
|
||||||
let mut eval = Evaluator::new(&module);
|
|
||||||
eval.eval_module(ast, &globals)
|
|
||||||
.map_err(|err| anyhow::anyhow!("{err}"))?;
|
|
||||||
drop(eval);
|
|
||||||
|
|
||||||
Ok(module)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parse and evaluate every `.star` file under `dir` into a single frozen
|
|
||||||
/// module whose public bindings can be imported into recipe modules. Returns
|
|
||||||
/// `Ok(None)` if `dir` doesn't exist or contains no `.star` files.
|
|
||||||
pub fn eval_lib(dir: &Path, options: Option<&Options>) -> anyhow::Result<Option<FrozenModule>> {
|
|
||||||
if !dir.exists() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut files: Vec<PathBuf> = Vec::new();
|
|
||||||
for entry in WalkDir::new(dir) {
|
|
||||||
let entry = entry.with_context(|| format!("walking lib directory {}", dir.display()))?;
|
|
||||||
let path = entry.path();
|
|
||||||
if entry.file_type().is_file() && path.extension().is_some_and(|ext| ext == "star") {
|
|
||||||
files.push(path.to_path_buf());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if files.is_empty() {
|
|
||||||
return Ok(None);
|
|
||||||
}
|
|
||||||
// Sorted for deterministic ordering when later definitions shadow earlier ones.
|
|
||||||
files.sort();
|
|
||||||
|
|
||||||
let module = Module::new();
|
|
||||||
if let Some(options) = options {
|
|
||||||
inject_options(&module, options);
|
|
||||||
}
|
|
||||||
let dialect = dialect();
|
|
||||||
let globals = globals();
|
|
||||||
|
|
||||||
for file in &files {
|
|
||||||
let ast = AstModule::parse_file(file, &dialect)
|
|
||||||
.map_err(|err| anyhow::anyhow!("parsing {}: {err}", file.display()))?;
|
|
||||||
let mut eval = Evaluator::new(&module);
|
|
||||||
eval.eval_module(ast, &globals)
|
|
||||||
.map_err(|err| anyhow::anyhow!("evaluating {}: {err}", file.display()))?;
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Some(module.freeze()?))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dialect() -> Dialect {
|
|
||||||
Dialect {
|
|
||||||
enable_top_level_stmt: true,
|
|
||||||
enable_f_strings: true,
|
|
||||||
..Dialect::Standard
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn globals() -> Globals {
|
|
||||||
GlobalsBuilder::standard().with(builder_globals).build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn inject_options(module: &Module, options: &Options) {
|
|
||||||
let value = module.heap().alloc(options.clone());
|
|
||||||
module.set("options", value);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_string(module: &Module, key: &str) -> Result<String, ExtractError> {
|
|
||||||
module
|
|
||||||
.get(key)
|
|
||||||
.ok_or_else(|| ExtractError::NotFound)
|
|
||||||
.and_then(|v| {
|
|
||||||
v.unpack_str()
|
|
||||||
.map(|v| v.to_string())
|
|
||||||
.ok_or_else(|| ExtractError::TypeMismatch)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_i32(module: &Module, key: &str) -> Result<i32, ExtractError> {
|
|
||||||
module
|
|
||||||
.get(key)
|
|
||||||
.ok_or(ExtractError::NotFound)
|
|
||||||
.and_then(|v| v.unpack_i32().ok_or(ExtractError::TypeMismatch))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn extract_string_list(module: &Module, key: &str) -> Result<Vec<String>, ExtractError> {
|
|
||||||
let value = module.get(key).ok_or(ExtractError::NotFound)?;
|
|
||||||
let list = ListRef::from_value(value).ok_or(ExtractError::TypeMismatch)?;
|
|
||||||
list.iter()
|
|
||||||
.map(|v| {
|
|
||||||
v.unpack_str()
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.ok_or(ExtractError::TypeMismatch)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
use crate::eval::Path;
|
||||||
|
|
||||||
|
use allocative::Allocative;
|
||||||
|
use starlark::{
|
||||||
|
collections::SmallMap,
|
||||||
|
environment::GlobalsBuilder,
|
||||||
|
eval::Evaluator,
|
||||||
|
starlark_module, starlark_simple_value,
|
||||||
|
values::{StarlarkValue, Value, ValueLike, none::NoneType},
|
||||||
|
};
|
||||||
|
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
|
||||||
|
use std::{
|
||||||
|
cell::Cell,
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path as StdPath, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
|
||||||
|
pub struct PodmanConfig {
|
||||||
|
image: String,
|
||||||
|
dockerfile: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for PodmanConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "podman_config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
starlark_simple_value!(PodmanConfig);
|
||||||
|
|
||||||
|
#[starlark_value(type = "podman_config")]
|
||||||
|
impl<'v> StarlarkValue<'v> for PodmanConfig {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
|
||||||
|
pub enum ContainerConfig {
|
||||||
|
Podman(PodmanConfig),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for ContainerConfig {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "container_config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
starlark_simple_value!(ContainerConfig);
|
||||||
|
|
||||||
|
#[starlark_value(type = "container_config")]
|
||||||
|
impl<'v> StarlarkValue<'v> for ContainerConfig {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Allocative, ProvidesStaticType)]
|
||||||
|
pub enum ConfigValue {
|
||||||
|
String(String),
|
||||||
|
Integer(i32),
|
||||||
|
Bool(bool),
|
||||||
|
Path(Path),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
|
||||||
|
pub struct Config {
|
||||||
|
arch: String,
|
||||||
|
container: ContainerConfig,
|
||||||
|
recipes_dir: Path,
|
||||||
|
host_recipes_dir: Path,
|
||||||
|
|
||||||
|
options: HashMap<String, ConfigValue>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn arch(&self) -> &str {
|
||||||
|
&self.arch
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn container(&self) -> &ContainerConfig {
|
||||||
|
&self.container
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn recipes_dir(&self) -> &StdPath {
|
||||||
|
&self.recipes_dir.path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn host_recipes_dir(&self) -> &StdPath {
|
||||||
|
&self.host_recipes_dir.path()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn options(&self) -> &HashMap<String, ConfigValue> {
|
||||||
|
&self.options
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Config {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "container_config")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
starlark_simple_value!(Config);
|
||||||
|
|
||||||
|
#[starlark_value(type = "config")]
|
||||||
|
impl<'v> StarlarkValue<'v> for Config {}
|
||||||
|
|
||||||
|
#[starlark_module]
|
||||||
|
pub fn config_globals(b: &mut GlobalsBuilder) {
|
||||||
|
fn podman(
|
||||||
|
#[starlark(require = named)] image: &str,
|
||||||
|
#[starlark(require = named)] dockerfile: &Path,
|
||||||
|
) -> anyhow::Result<ContainerConfig> {
|
||||||
|
Ok(ContainerConfig::Podman(PodmanConfig {
|
||||||
|
image: image.to_string(),
|
||||||
|
dockerfile: dockerfile.path().to_owned(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config(
|
||||||
|
#[starlark(require = named)] arch: &str,
|
||||||
|
#[starlark(require = named)] container: &ContainerConfig,
|
||||||
|
#[starlark(require = named)] recipes_dir: &Path,
|
||||||
|
#[starlark(require = named)] host_recipes_dir: &Path,
|
||||||
|
#[starlark(kwargs)] kwargs: SmallMap<&str, Value>,
|
||||||
|
eval: &mut Evaluator,
|
||||||
|
) -> anyhow::Result<NoneType> {
|
||||||
|
let config = eval
|
||||||
|
.extra
|
||||||
|
.and_then(|extra| extra.downcast_ref::<Cell<Option<Config>>>())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("`config` called outside of config.star"))?;
|
||||||
|
|
||||||
|
config.set(Some(Config {
|
||||||
|
arch: arch.to_string(),
|
||||||
|
container: container.clone(),
|
||||||
|
recipes_dir: recipes_dir.clone(),
|
||||||
|
host_recipes_dir: host_recipes_dir.clone(),
|
||||||
|
options: kwargs
|
||||||
|
.iter()
|
||||||
|
.map(|(&k, v)| {
|
||||||
|
let value = if let Some(str) = v.unpack_str() {
|
||||||
|
ConfigValue::String(str.to_string())
|
||||||
|
} else if let Some(num) = v.unpack_i32() {
|
||||||
|
ConfigValue::Integer(num)
|
||||||
|
} else if let Some(bool) = v.unpack_bool() {
|
||||||
|
ConfigValue::Bool(bool)
|
||||||
|
} else if let Some(path) = v.downcast_ref::<Path>() {
|
||||||
|
ConfigValue::Path(path.clone())
|
||||||
|
} else {
|
||||||
|
anyhow::bail!("config option must be a `string`, `int`, `bool` or `path`");
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok((k.to_string(), value))
|
||||||
|
})
|
||||||
|
.collect::<Result<_, _>>()?,
|
||||||
|
}));
|
||||||
|
|
||||||
|
Ok(NoneType)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
use starlark::{
|
||||||
|
any::AnyLifetime,
|
||||||
|
environment::{FrozenModule, Globals, Module},
|
||||||
|
eval::Evaluator,
|
||||||
|
syntax::{AstModule, Dialect, DialectTypes},
|
||||||
|
values::{UnpackValue, Value, type_repr::StarlarkTypeRepr},
|
||||||
|
};
|
||||||
|
use std::path::Path as StdPath;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod recipe;
|
||||||
|
mod types;
|
||||||
|
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use config::*;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use recipe::*;
|
||||||
|
#[allow(unused_imports)]
|
||||||
|
pub use types::*;
|
||||||
|
|
||||||
|
pub trait UnpackCloned: Sized + StarlarkTypeRepr {
|
||||||
|
fn unpack_cloned(value: Value<'_>) -> Option<Self>;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> UnpackCloned for T
|
||||||
|
where
|
||||||
|
for<'v> T: UnpackValue<'v>,
|
||||||
|
{
|
||||||
|
fn unpack_cloned(value: Value<'_>) -> Option<Self> {
|
||||||
|
T::unpack_value(value).unwrap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_files(
|
||||||
|
path: &[&StdPath],
|
||||||
|
globals: &Globals,
|
||||||
|
lib_module: Option<&FrozenModule>,
|
||||||
|
config: Option<&Config>,
|
||||||
|
extra: Option<&dyn AnyLifetime>,
|
||||||
|
) -> anyhow::Result<Module> {
|
||||||
|
use anyhow::Context;
|
||||||
|
|
||||||
|
let module = Module::new();
|
||||||
|
|
||||||
|
if let Some(lib_module) = lib_module {
|
||||||
|
module.import_public_symbols(lib_module);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(config) = config {
|
||||||
|
module.set("options", module.heap().alloc(config.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut paths = path.to_vec();
|
||||||
|
|
||||||
|
paths.sort();
|
||||||
|
|
||||||
|
let ast_modules = paths
|
||||||
|
.iter()
|
||||||
|
.map(|&path| {
|
||||||
|
let module = AstModule::parse_file(path, &default_dialect())
|
||||||
|
.map_err(|err| anyhow::anyhow!("{err}"))
|
||||||
|
.context(format!("parsing file {:?}", path.display()))?;
|
||||||
|
|
||||||
|
Ok((path, module))
|
||||||
|
})
|
||||||
|
.collect::<anyhow::Result<Vec<(&StdPath, AstModule)>>>()?;
|
||||||
|
|
||||||
|
for (path, ast) in ast_modules {
|
||||||
|
let mut eval = Evaluator::new(&module);
|
||||||
|
|
||||||
|
if let Some(extra) = extra {
|
||||||
|
eval.extra = Some(extra);
|
||||||
|
}
|
||||||
|
|
||||||
|
eval.eval_module(ast, globals)
|
||||||
|
.map_err(|err| anyhow::anyhow!("{err}"))
|
||||||
|
.context(format!("evaluating file {:?}", path.display()))?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(module)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_dialect() -> Dialect {
|
||||||
|
Dialect {
|
||||||
|
enable_def: true,
|
||||||
|
enable_lambda: true,
|
||||||
|
enable_load: false,
|
||||||
|
enable_keyword_only_arguments: false,
|
||||||
|
enable_positional_only_arguments: false,
|
||||||
|
enable_types: DialectTypes::Disable,
|
||||||
|
enable_load_reexport: false,
|
||||||
|
enable_top_level_stmt: true,
|
||||||
|
enable_f_strings: true,
|
||||||
|
..Dialect::Standard
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
use std::cell::Cell;
|
||||||
|
|
||||||
|
use allocative::Allocative;
|
||||||
|
use starlark::{
|
||||||
|
environment::{GlobalsBuilder, Methods, MethodsBuilder, MethodsStatic},
|
||||||
|
eval::Evaluator,
|
||||||
|
starlark_module, starlark_simple_value,
|
||||||
|
typing::Ty,
|
||||||
|
values::{
|
||||||
|
Heap, StarlarkValue, UnpackValue, Value, ValueLike, none::NoneType, tuple::UnpackTuple,
|
||||||
|
type_repr::StarlarkTypeRepr,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
container::{Container, ContainerManager},
|
||||||
|
eval::{Path, UnpackCloned},
|
||||||
|
log,
|
||||||
|
recipe::Source,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
|
||||||
|
pub struct TarballSource {
|
||||||
|
url: String,
|
||||||
|
sha256: String,
|
||||||
|
strip_components: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for TarballSource {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "tarball")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
starlark_simple_value!(TarballSource);
|
||||||
|
|
||||||
|
#[starlark_value(type = "tarball")]
|
||||||
|
impl<'v> StarlarkValue<'v> for TarballSource {}
|
||||||
|
|
||||||
|
impl UnpackCloned for TarballSource {
|
||||||
|
fn unpack_cloned(value: Value<'_>) -> Option<Self> {
|
||||||
|
value.downcast_ref().cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Source for TarballSource {}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
|
||||||
|
pub struct Metadata {
|
||||||
|
maintainer: Option<String>,
|
||||||
|
description: Option<String>,
|
||||||
|
license: Option<String>,
|
||||||
|
website: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Metadata {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "metadata")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
starlark_simple_value!(Metadata);
|
||||||
|
|
||||||
|
#[starlark_value(type = "metadata")]
|
||||||
|
impl<'v> StarlarkValue<'v> for Metadata {}
|
||||||
|
|
||||||
|
impl UnpackCloned for Metadata {
|
||||||
|
fn unpack_cloned(value: Value<'_>) -> Option<Self> {
|
||||||
|
value.downcast_ref().cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
|
||||||
|
pub struct Context {
|
||||||
|
pub source_dir: Path,
|
||||||
|
pub build_dir: Path,
|
||||||
|
pub jobs: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Context {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "context")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
starlark_simple_value!(Context);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct RunArg(pub String);
|
||||||
|
|
||||||
|
impl UnpackValue<'_> for RunArg {
|
||||||
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
|
fn unpack_value_impl(value: Value) -> anyhow::Result<Option<Self>> {
|
||||||
|
Ok(if let Some(str) = value.unpack_str() {
|
||||||
|
Some(RunArg(str.to_owned()))
|
||||||
|
} else if let Some(int) = value.unpack_i32() {
|
||||||
|
Some(RunArg(int.to_string()))
|
||||||
|
} else if let Some(path) = value.downcast_ref::<Path>() {
|
||||||
|
Some(RunArg(path.path().to_str().unwrap_or("").to_string()))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StarlarkTypeRepr for RunArg {
|
||||||
|
type Canonical = Self;
|
||||||
|
|
||||||
|
fn starlark_type_repr() -> starlark::typing::Ty {
|
||||||
|
Ty::string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(ProvidesStaticType)]
|
||||||
|
pub struct ContainerManagerWrapper<'a>(pub &'a ContainerManager);
|
||||||
|
|
||||||
|
#[starlark_module]
|
||||||
|
fn context_methods(b: &mut MethodsBuilder) {
|
||||||
|
fn run(
|
||||||
|
#[starlark(this)] this: &Context,
|
||||||
|
#[starlark(args)] args: UnpackTuple<RunArg>,
|
||||||
|
eval: &mut Evaluator,
|
||||||
|
) -> anyhow::Result<NoneType> {
|
||||||
|
let ContainerManagerWrapper(container_manager) = eval
|
||||||
|
.extra
|
||||||
|
.and_then(|extra| extra.downcast_ref())
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("`config` called outside of config.star"))?;
|
||||||
|
|
||||||
|
let argv = args.items.iter().map(|x| x.0.as_str()).collect::<Vec<_>>();
|
||||||
|
|
||||||
|
log!("run", "Running command: {argv:?}");
|
||||||
|
|
||||||
|
container_manager
|
||||||
|
.container("changeme")? // TODO
|
||||||
|
.exec(argv, [], std::path::Path::new("/"))?;
|
||||||
|
|
||||||
|
Ok(NoneType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[starlark_value(type = "context")]
|
||||||
|
impl<'v> StarlarkValue<'v> for Context {
|
||||||
|
fn get_methods() -> Option<&'static Methods> {
|
||||||
|
static RES: MethodsStatic = MethodsStatic::new();
|
||||||
|
RES.methods(context_methods)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_attr(&self, attr: &str, _heap: &Heap) -> bool {
|
||||||
|
match attr {
|
||||||
|
"source_dir" => true,
|
||||||
|
"build_dir" => true,
|
||||||
|
"jobs" => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option<Value<'v>> {
|
||||||
|
match attr {
|
||||||
|
"source_dir" => Some(heap.alloc(self.source_dir.clone())),
|
||||||
|
"build_dir" => Some(heap.alloc(self.build_dir.clone())),
|
||||||
|
"jobs" => Some(heap.alloc(self.jobs)),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UnpackCloned for Context {
|
||||||
|
fn unpack_cloned(value: Value<'_>) -> Option<Self> {
|
||||||
|
value.downcast_ref().cloned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[starlark_module]
|
||||||
|
pub fn recipe_globals(b: &mut GlobalsBuilder) {
|
||||||
|
fn tarball(
|
||||||
|
#[starlark(require = named)] url: &str,
|
||||||
|
#[starlark(require = named)] sha256: &str,
|
||||||
|
#[starlark(require = named, default = 0)] strip_components: u32,
|
||||||
|
) -> anyhow::Result<TarballSource> {
|
||||||
|
Ok(TarballSource {
|
||||||
|
url: url.to_string(),
|
||||||
|
sha256: sha256.to_string(),
|
||||||
|
strip_components,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn meta(
|
||||||
|
#[starlark(require = named)] maintainer: Option<&str>,
|
||||||
|
#[starlark(require = named)] description: Option<&str>,
|
||||||
|
#[starlark(require = named)] license: Option<&str>,
|
||||||
|
#[starlark(require = named)] website: Option<&str>,
|
||||||
|
) -> anyhow::Result<Metadata> {
|
||||||
|
Ok(Metadata {
|
||||||
|
maintainer: maintainer.map(|x| x.to_string()),
|
||||||
|
description: description.map(|x| x.to_string()),
|
||||||
|
license: license.map(|x| x.to_string()),
|
||||||
|
website: website.map(|x| x.to_string()),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use allocative::Allocative;
|
||||||
|
use starlark::{
|
||||||
|
environment::GlobalsBuilder,
|
||||||
|
starlark_module, starlark_simple_value,
|
||||||
|
values::{Heap, StarlarkValue, Value, ValueLike},
|
||||||
|
};
|
||||||
|
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
|
||||||
|
use std::path::{Path as StdPath, PathBuf};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Allocative, NoSerialize, ProvidesStaticType)]
|
||||||
|
pub struct Path {
|
||||||
|
inner: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Path {
|
||||||
|
pub fn new(value: impl Into<PathBuf>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: value.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn path(&self) -> &StdPath {
|
||||||
|
&self.inner
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Display for Path {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
write!(f, "path({:?})", self.inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
starlark_simple_value!(Path);
|
||||||
|
|
||||||
|
#[starlark_value(type = "path")]
|
||||||
|
impl<'v> StarlarkValue<'v> for Path {
|
||||||
|
fn div(&self, other: Value<'v>, heap: &'v Heap) -> starlark::Result<Value<'v>> {
|
||||||
|
let rhs = if let Some(str) = other.unpack_str() {
|
||||||
|
str.to_string()
|
||||||
|
} else if let Some(path) = other.downcast_ref::<Path>() {
|
||||||
|
path.inner.to_str().unwrap_or("").to_string()
|
||||||
|
} else {
|
||||||
|
return Err(starlark::Error::new_other(anyhow::anyhow!(
|
||||||
|
"expected a `string` or `path`, got `{}`",
|
||||||
|
other.get_type()
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(heap.alloc(Path {
|
||||||
|
inner: self
|
||||||
|
.inner
|
||||||
|
.join(rhs.trim_start_matches(std::path::MAIN_SEPARATOR_STR)),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[starlark_module]
|
||||||
|
pub fn types_globals(b: &mut GlobalsBuilder) {
|
||||||
|
fn path(value: &str) -> anyhow::Result<Path> {
|
||||||
|
Ok(Path {
|
||||||
|
inner: PathBuf::from(value),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
-385
@@ -1,385 +0,0 @@
|
|||||||
use std::{
|
|
||||||
collections::{BTreeMap, BTreeSet},
|
|
||||||
fmt, fs,
|
|
||||||
path::Path,
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::bail;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
layout::Layout,
|
|
||||||
recipe::{OutputPackage, Recipe, RecipeKind, RecipeSet},
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
|
||||||
pub enum TaskId {
|
|
||||||
FetchSources(String),
|
|
||||||
PrepareSources(String),
|
|
||||||
ConfigureRecipe(String),
|
|
||||||
BuildRecipe(String),
|
|
||||||
InstallPackageFiles(String),
|
|
||||||
ProduceApk(String),
|
|
||||||
InstallHostRecipe(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for TaskId {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self {
|
|
||||||
Self::FetchSources(recipe) => write!(f, "fetch sources {recipe}"),
|
|
||||||
Self::PrepareSources(recipe) => write!(f, "prepare sources {recipe}"),
|
|
||||||
Self::ConfigureRecipe(recipe) => write!(f, "configure {recipe}"),
|
|
||||||
Self::BuildRecipe(recipe) => write!(f, "build {recipe}"),
|
|
||||||
Self::InstallPackageFiles(output) => write!(f, "install package files {output}"),
|
|
||||||
Self::ProduceApk(output) => write!(f, "produce apk {output}"),
|
|
||||||
Self::InstallHostRecipe(recipe) => write!(f, "install host recipe {recipe}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct TaskPlan {
|
|
||||||
dependencies: BTreeMap<TaskId, Vec<TaskId>>,
|
|
||||||
order: Vec<TaskId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TaskPlan {
|
|
||||||
pub fn order(&self) -> &[TaskId] {
|
|
||||||
&self.order
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_empty(&self) -> bool {
|
|
||||||
self.order.is_empty()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dependency_count(&self) -> usize {
|
|
||||||
self.dependencies.values().map(Vec::len).sum()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
pub fn dependencies(&self, task: &TaskId) -> Option<&[TaskId]> {
|
|
||||||
self.dependencies.get(task).map(Vec::as_slice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TaskPlanner<'a> {
|
|
||||||
layout: Layout<'a>,
|
|
||||||
recipes: &'a RecipeSet,
|
|
||||||
force: bool,
|
|
||||||
dependencies: BTreeMap<TaskId, Vec<TaskId>>,
|
|
||||||
inactive: BTreeSet<TaskId>,
|
|
||||||
visiting: BTreeSet<TaskId>,
|
|
||||||
visited: BTreeSet<TaskId>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> TaskPlanner<'a> {
|
|
||||||
pub fn new(root: &'a Path, arch: &'a str, recipes: &'a RecipeSet) -> Self {
|
|
||||||
Self {
|
|
||||||
layout: Layout::new(root, arch),
|
|
||||||
recipes,
|
|
||||||
force: false,
|
|
||||||
dependencies: BTreeMap::new(),
|
|
||||||
inactive: BTreeSet::new(),
|
|
||||||
visiting: BTreeSet::new(),
|
|
||||||
visited: BTreeSet::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_plan(mut self, requests: &[String], force: bool) -> anyhow::Result<TaskPlan> {
|
|
||||||
self.force = force;
|
|
||||||
for request in requests {
|
|
||||||
let recipe = self.recipes.recipe(request)?;
|
|
||||||
match recipe.kind() {
|
|
||||||
RecipeKind::Package => {
|
|
||||||
for output in recipe.outputs() {
|
|
||||||
self.visit(TaskId::ProduceApk(output.key()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
RecipeKind::HostPackage => {
|
|
||||||
self.visit(TaskId::InstallHostRecipe(recipe.key()))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.into_plan()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn fetch_plan(mut self, requests: &[String]) -> anyhow::Result<TaskPlan> {
|
|
||||||
for request in requests {
|
|
||||||
let recipe = self.recipes.recipe(request)?;
|
|
||||||
self.visit(TaskId::FetchSources(recipe.key()))?;
|
|
||||||
}
|
|
||||||
self.into_plan()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn visit(&mut self, task: TaskId) -> anyhow::Result<()> {
|
|
||||||
if self.visited.contains(&task) || self.inactive.contains(&task) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if !self.is_active(&task)? {
|
|
||||||
self.inactive.insert(task);
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if !self.visiting.insert(task.clone()) {
|
|
||||||
bail!("task dependency cycle involving `{task}`");
|
|
||||||
}
|
|
||||||
|
|
||||||
let dependencies = self.dependencies(&task)?;
|
|
||||||
let mut active_dependencies = Vec::new();
|
|
||||||
for dependency in dependencies {
|
|
||||||
self.visit(dependency.clone())?;
|
|
||||||
if self.dependencies.contains_key(&dependency) {
|
|
||||||
active_dependencies.push(dependency);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
self.visiting.remove(&task);
|
|
||||||
self.visited.insert(task.clone());
|
|
||||||
self.dependencies.insert(task, active_dependencies);
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn into_plan(self) -> anyhow::Result<TaskPlan> {
|
|
||||||
let order = recipe_contiguous_order(&self.dependencies, self.recipes)?;
|
|
||||||
Ok(TaskPlan {
|
|
||||||
dependencies: self.dependencies,
|
|
||||||
order,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dependencies(&self, task: &TaskId) -> anyhow::Result<Vec<TaskId>> {
|
|
||||||
match task {
|
|
||||||
TaskId::FetchSources(_) => Ok(Vec::new()),
|
|
||||||
TaskId::PrepareSources(recipe) => Ok(vec![TaskId::FetchSources(recipe.clone())]),
|
|
||||||
TaskId::ConfigureRecipe(recipe) => {
|
|
||||||
let recipe = self.recipes.recipe(recipe)?;
|
|
||||||
let mut deps = vec![TaskId::PrepareSources(recipe.key())];
|
|
||||||
deps.extend(
|
|
||||||
recipe
|
|
||||||
.host_deps()
|
|
||||||
.iter()
|
|
||||||
.map(|dep| TaskId::InstallHostRecipe(RecipeKind::HostPackage.key(dep))),
|
|
||||||
);
|
|
||||||
deps.extend(
|
|
||||||
recipe
|
|
||||||
.build_deps()
|
|
||||||
.iter()
|
|
||||||
.chain(recipe.deps().iter())
|
|
||||||
.map(|dep| TaskId::ProduceApk(dep.clone())),
|
|
||||||
);
|
|
||||||
Ok(deps)
|
|
||||||
}
|
|
||||||
TaskId::BuildRecipe(recipe) => Ok(vec![TaskId::ConfigureRecipe(recipe.clone())]),
|
|
||||||
TaskId::InstallPackageFiles(output) => {
|
|
||||||
let output = self.recipes.output(output)?;
|
|
||||||
Ok(vec![TaskId::BuildRecipe(output.recipe().to_owned())])
|
|
||||||
}
|
|
||||||
TaskId::ProduceApk(output) => {
|
|
||||||
let output = self.recipes.output(output)?;
|
|
||||||
let recipe = self.recipes.recipe(output.recipe())?;
|
|
||||||
let mut deps = vec![TaskId::InstallPackageFiles(output.key())];
|
|
||||||
deps.extend(
|
|
||||||
recipe
|
|
||||||
.deps()
|
|
||||||
.iter()
|
|
||||||
.chain(recipe.run_deps().iter())
|
|
||||||
.map(|dep| TaskId::ProduceApk(dep.clone())),
|
|
||||||
);
|
|
||||||
Ok(deps)
|
|
||||||
}
|
|
||||||
TaskId::InstallHostRecipe(recipe) => {
|
|
||||||
self.recipes.recipe(recipe)?;
|
|
||||||
Ok(vec![TaskId::BuildRecipe(recipe.clone())])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_active(&self, task: &TaskId) -> anyhow::Result<bool> {
|
|
||||||
match task {
|
|
||||||
TaskId::FetchSources(recipe) => self.fetch_sources_active(self.recipes.recipe(recipe)?),
|
|
||||||
TaskId::PrepareSources(recipe) => {
|
|
||||||
self.prepare_sources_active(self.recipes.recipe(recipe)?)
|
|
||||||
}
|
|
||||||
TaskId::ConfigureRecipe(recipe) => {
|
|
||||||
self.recipe_task_active(self.recipes.recipe(recipe)?, "configure")
|
|
||||||
}
|
|
||||||
TaskId::BuildRecipe(recipe) => {
|
|
||||||
self.recipe_task_active(self.recipes.recipe(recipe)?, "build")
|
|
||||||
}
|
|
||||||
TaskId::InstallPackageFiles(output) => {
|
|
||||||
let output = self.recipes.output(output)?;
|
|
||||||
let recipe = self.recipes.recipe(output.recipe())?;
|
|
||||||
Ok(self.output_task_active(recipe, output, "install")?
|
|
||||||
|| self.produce_apk_active(recipe, output)?)
|
|
||||||
}
|
|
||||||
TaskId::ProduceApk(output) => {
|
|
||||||
let output = self.recipes.output(output)?;
|
|
||||||
let recipe = self.recipes.recipe(output.recipe())?;
|
|
||||||
self.produce_apk_active(recipe, output)
|
|
||||||
}
|
|
||||||
TaskId::InstallHostRecipe(recipe) => {
|
|
||||||
let recipe = self.recipes.recipe(recipe)?;
|
|
||||||
self.install_host_recipe_active(recipe)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fetch_sources_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
|
|
||||||
Ok(recipe.sources().entries().iter().any(|(_, source)| {
|
|
||||||
source.is_unknown_cache_key()
|
|
||||||
|| !self.layout.source_cache_path(source.cache_key()).exists()
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prepare_sources_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
|
|
||||||
if self.force {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
let want_version = format!("{}-r{}", recipe.version(), recipe.revision());
|
|
||||||
if fs::read_to_string(self.layout.source_stamp(recipe, "version"))
|
|
||||||
.ok()
|
|
||||||
.as_deref()
|
|
||||||
!= Some(want_version.as_str())
|
|
||||||
{
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
if self.layout.recipe_has_patches(recipe)?
|
|
||||||
&& !self.layout.source_stamp(recipe, "patched").exists()
|
|
||||||
{
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recipe_task_active(&self, recipe: &Recipe, kind: &str) -> anyhow::Result<bool> {
|
|
||||||
if self.force {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Ok(
|
|
||||||
fs::read_to_string(self.layout.recipe_task_stamp(recipe, kind))
|
|
||||||
.ok()
|
|
||||||
.as_deref()
|
|
||||||
!= Some(self.layout.recipe_fingerprint(recipe)?.as_str()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn output_task_active(
|
|
||||||
&self,
|
|
||||||
recipe: &Recipe,
|
|
||||||
output: &OutputPackage,
|
|
||||||
kind: &str,
|
|
||||||
) -> anyhow::Result<bool> {
|
|
||||||
if self.force {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Ok(
|
|
||||||
fs::read_to_string(self.layout.output_task_stamp(output, kind))
|
|
||||||
.ok()
|
|
||||||
.as_deref()
|
|
||||||
!= Some(self.layout.output_fingerprint(recipe, output)?.as_str()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn produce_apk_active(&self, recipe: &Recipe, output: &OutputPackage) -> anyhow::Result<bool> {
|
|
||||||
if self.force {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
if !self.layout.apk_path(recipe, output).exists() {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Ok(
|
|
||||||
fs::read_to_string(self.layout.output_task_stamp(output, "apk"))
|
|
||||||
.ok()
|
|
||||||
.as_deref()
|
|
||||||
!= Some(self.layout.output_fingerprint(recipe, output)?.as_str()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn install_host_recipe_active(&self, recipe: &Recipe) -> anyhow::Result<bool> {
|
|
||||||
if self.force {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
if !self.layout.host_install_dir(recipe).exists() {
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
Ok(
|
|
||||||
fs::read_to_string(self.layout.recipe_task_stamp(recipe, "host-install"))
|
|
||||||
.ok()
|
|
||||||
.as_deref()
|
|
||||||
!= Some(self.layout.recipe_fingerprint(recipe)?.as_str()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn recipe_contiguous_order(
|
|
||||||
dependencies: &BTreeMap<TaskId, Vec<TaskId>>,
|
|
||||||
recipes: &RecipeSet,
|
|
||||||
) -> anyhow::Result<Vec<TaskId>> {
|
|
||||||
let total = dependencies.len();
|
|
||||||
let mut remaining: BTreeMap<TaskId, BTreeSet<TaskId>> = dependencies
|
|
||||||
.iter()
|
|
||||||
.map(|(task, deps)| (task.clone(), deps.iter().cloned().collect()))
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let mut dependents: BTreeMap<TaskId, Vec<TaskId>> = BTreeMap::new();
|
|
||||||
for (task, deps) in dependencies {
|
|
||||||
for dep in deps {
|
|
||||||
dependents
|
|
||||||
.entry(dep.clone())
|
|
||||||
.or_default()
|
|
||||||
.push(task.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut order = Vec::with_capacity(total);
|
|
||||||
let mut current_recipe: Option<String> = None;
|
|
||||||
while order.len() < total {
|
|
||||||
let next = pick_next(&remaining, current_recipe.as_deref(), recipes)?;
|
|
||||||
let slug = task_recipe_slug(&next, recipes)?;
|
|
||||||
current_recipe = Some(slug);
|
|
||||||
remaining.remove(&next);
|
|
||||||
if let Some(children) = dependents.get(&next) {
|
|
||||||
for child in children {
|
|
||||||
if let Some(deps) = remaining.get_mut(child) {
|
|
||||||
deps.remove(&next);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
order.push(next);
|
|
||||||
}
|
|
||||||
Ok(order)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pick_next(
|
|
||||||
remaining: &BTreeMap<TaskId, BTreeSet<TaskId>>,
|
|
||||||
current_recipe: Option<&str>,
|
|
||||||
recipes: &RecipeSet,
|
|
||||||
) -> anyhow::Result<TaskId> {
|
|
||||||
let mut fallback: Option<TaskId> = None;
|
|
||||||
for (task, deps) in remaining {
|
|
||||||
if !deps.is_empty() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if let Some(active) = current_recipe {
|
|
||||||
let slug = task_recipe_slug(task, recipes)?;
|
|
||||||
if slug == active {
|
|
||||||
return Ok(task.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fallback.is_none() {
|
|
||||||
fallback = Some(task.clone());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
fallback.ok_or_else(|| anyhow::anyhow!("task dependency cycle detected"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn task_recipe_slug(task: &TaskId, recipes: &RecipeSet) -> anyhow::Result<String> {
|
|
||||||
Ok(match task {
|
|
||||||
TaskId::FetchSources(recipe)
|
|
||||||
| TaskId::PrepareSources(recipe)
|
|
||||||
| TaskId::ConfigureRecipe(recipe)
|
|
||||||
| TaskId::BuildRecipe(recipe)
|
|
||||||
| TaskId::InstallHostRecipe(recipe) => recipe.clone(),
|
|
||||||
TaskId::InstallPackageFiles(output) | TaskId::ProduceApk(output) => {
|
|
||||||
recipes.output(output)?.recipe().to_owned()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
-131
@@ -1,131 +0,0 @@
|
|||||||
use std::{
|
|
||||||
fs,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
|
|
||||||
use anyhow::Context;
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
|
|
||||||
use crate::recipe::{OutputPackage, Recipe};
|
|
||||||
|
|
||||||
pub struct Layout<'a> {
|
|
||||||
pub root: &'a Path,
|
|
||||||
pub arch: &'a str,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> Layout<'a> {
|
|
||||||
pub fn new(root: &'a Path, arch: &'a str) -> Self {
|
|
||||||
Self { root, arch }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn source_cache_dir(&self) -> PathBuf {
|
|
||||||
self.root.join("build/cache/sources")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn source_cache_path(&self, key: &str) -> PathBuf {
|
|
||||||
self.source_cache_dir().join(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn source_workdir(&self, recipe: &Recipe) -> PathBuf {
|
|
||||||
self.root.join("build/sources").join(recipe.slug())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_workdir(&self, recipe: &Recipe) -> PathBuf {
|
|
||||||
self.root.join("build/builds").join(recipe.slug())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn host_install_dir(&self, recipe: &Recipe) -> PathBuf {
|
|
||||||
self.root
|
|
||||||
.join("build/host-pkgs")
|
|
||||||
.join(recipe.slug())
|
|
||||||
.join("usr/local")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn host_install_root(&self, recipe: &Recipe) -> PathBuf {
|
|
||||||
self.root.join("build/host-pkgs").join(recipe.slug())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn apk_path(&self, recipe: &Recipe, output: &OutputPackage) -> PathBuf {
|
|
||||||
self.root.join("build/pkgs").join(self.arch).join(format!(
|
|
||||||
"{}-{}-r{}.apk",
|
|
||||||
output.name(),
|
|
||||||
recipe.version(),
|
|
||||||
recipe.revision()
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn source_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf {
|
|
||||||
self.root
|
|
||||||
.join("build/sources")
|
|
||||||
.join(format!("{}.{kind}", recipe.slug()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recipe_task_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf {
|
|
||||||
self.root
|
|
||||||
.join("build/tasks")
|
|
||||||
.join(format!("{}.{kind}", recipe.slug()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn output_task_stamp(&self, output: &OutputPackage, kind: &str) -> PathBuf {
|
|
||||||
self.root
|
|
||||||
.join("build/tasks")
|
|
||||||
.join(format!("{}.{kind}", output.key().replace(':', "-")))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recipe_fingerprint(&self, recipe: &Recipe) -> anyhow::Result<String> {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(self.arch.as_bytes());
|
|
||||||
hasher.update(recipe.key().as_bytes());
|
|
||||||
hasher.update(recipe.version().as_bytes());
|
|
||||||
hasher.update(recipe.revision().to_le_bytes());
|
|
||||||
hasher.update(
|
|
||||||
fs::read(recipe.path())
|
|
||||||
.with_context(|| format!("reading recipe {}", recipe.path().display()))?,
|
|
||||||
);
|
|
||||||
for (name, source) in recipe.sources().entries() {
|
|
||||||
hasher.update(name.unwrap_or("").as_bytes());
|
|
||||||
hasher.update(source.url().as_bytes());
|
|
||||||
hasher.update(source.cache_key().as_bytes());
|
|
||||||
}
|
|
||||||
for patch in self.recipe_patches(recipe)? {
|
|
||||||
hasher.update(patch.display().to_string().as_bytes());
|
|
||||||
hasher
|
|
||||||
.update(fs::read(&patch).with_context(|| format!("reading {}", patch.display()))?);
|
|
||||||
}
|
|
||||||
Ok(hex::encode(hasher.finalize()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn output_fingerprint(
|
|
||||||
&self,
|
|
||||||
recipe: &Recipe,
|
|
||||||
output: &OutputPackage,
|
|
||||||
) -> anyhow::Result<String> {
|
|
||||||
let mut hasher = Sha256::new();
|
|
||||||
hasher.update(self.recipe_fingerprint(recipe)?.as_bytes());
|
|
||||||
hasher.update(output.key().as_bytes());
|
|
||||||
Ok(hex::encode(hasher.finalize()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recipe_has_patches(&self, recipe: &Recipe) -> anyhow::Result<bool> {
|
|
||||||
Ok(!self.recipe_patches(recipe)?.is_empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recipe_patches(&self, recipe: &Recipe) -> anyhow::Result<Vec<PathBuf>> {
|
|
||||||
let patches_dir = recipe.dir().join("patches");
|
|
||||||
if !patches_dir.exists() {
|
|
||||||
return Ok(Vec::new());
|
|
||||||
}
|
|
||||||
let mut patches = Vec::new();
|
|
||||||
for entry in fs::read_dir(&patches_dir)
|
|
||||||
.with_context(|| format!("reading patches directory {}", patches_dir.display()))?
|
|
||||||
{
|
|
||||||
let entry = entry?;
|
|
||||||
let path = entry.path();
|
|
||||||
if path.is_file() {
|
|
||||||
patches.push(path);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
patches.sort();
|
|
||||||
Ok(patches)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+8
-14
@@ -2,25 +2,19 @@ use std::{io::IsTerminal, sync::LazyLock};
|
|||||||
|
|
||||||
static IS_STDERR_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stderr().is_terminal());
|
static IS_STDERR_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stderr().is_terminal());
|
||||||
|
|
||||||
|
pub fn __emit(color: &str, action: &str, args: std::fmt::Arguments) {
|
||||||
const ARROW: &str = "==>";
|
const ARROW: &str = "==>";
|
||||||
|
|
||||||
fn emit(color: &str, action: &str, details: &str) {
|
|
||||||
if *IS_STDERR_TERMINAL {
|
if *IS_STDERR_TERMINAL {
|
||||||
eprintln!("\x1b[{color}m{ARROW} \x1b[1m{action} \x1b[0m{details}");
|
eprintln!("\x1b[{color}m{ARROW}\x1b[0m \x1b[1m{action}\x1b[0m {args}");
|
||||||
} else {
|
} else {
|
||||||
eprintln!("{ARROW} {action} {details}");
|
eprintln!("{ARROW} {action} {args}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn step(action: &str, details: &str) {
|
#[macro_export]
|
||||||
emit("1;34", action, details);
|
macro_rules! log {
|
||||||
}
|
($action:literal, $($arg:tt)*) => {{
|
||||||
|
$crate::log::__emit("1;34", $action, format_args!($($arg)*));
|
||||||
pub fn skip(action: &str, details: &str) {
|
}};
|
||||||
emit("1;33", action, details);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(dead_code)]
|
|
||||||
pub fn info(action: &str, details: &str) {
|
|
||||||
emit("1;32", action, details);
|
|
||||||
}
|
}
|
||||||
|
|||||||
+127
-6
@@ -1,15 +1,136 @@
|
|||||||
mod builder;
|
use crate::{
|
||||||
|
container::{ContainerManager, PodmanRuntime},
|
||||||
|
plan::{Plan, PlanKey},
|
||||||
|
recipe::{PackageRecipe, RecipeSet, SourceRecipe, ToolRecipe},
|
||||||
|
};
|
||||||
|
|
||||||
|
use std::{path::Path, sync::Arc};
|
||||||
|
|
||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
|
||||||
mod container;
|
mod container;
|
||||||
mod eval;
|
mod eval;
|
||||||
mod graph;
|
|
||||||
mod layout;
|
|
||||||
mod log;
|
mod log;
|
||||||
mod options;
|
mod plan;
|
||||||
mod phase;
|
|
||||||
mod recipe;
|
mod recipe;
|
||||||
|
|
||||||
fn main() -> anyhow::Result<()> {
|
fn main() -> anyhow::Result<()> {
|
||||||
cli::run()
|
cli::run()
|
||||||
|
|
||||||
|
// let podman_runtime = Arc::new(PodmanRuntime::new()?);
|
||||||
|
// let mut container_manager = ContainerManager::new(podman_runtime);
|
||||||
|
|
||||||
|
// container_manager.container("example")?.exec(
|
||||||
|
// vec!["sh", "-c", "uname -a && id"],
|
||||||
|
// vec![],
|
||||||
|
// Path::new("/"),
|
||||||
|
// )?;
|
||||||
|
|
||||||
|
// let mut recipes = RecipeSet::default();
|
||||||
|
|
||||||
|
// recipes.load_recipes(Path::new("./recipes"), Path::new("./host-recipes"))?;
|
||||||
|
|
||||||
|
// recipes.add_source(
|
||||||
|
// "binutils-2.46",
|
||||||
|
// SourceRecipe {
|
||||||
|
// name: "binutils-2.46".into(),
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_source(
|
||||||
|
// "gcc-16.1.0",
|
||||||
|
// SourceRecipe {
|
||||||
|
// name: "gcc-16.1.0".into(),
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_source(
|
||||||
|
// "linux-7.0.9",
|
||||||
|
// SourceRecipe {
|
||||||
|
// name: "linux-7.0.9".into(),
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_source(
|
||||||
|
// "glibc-2.41",
|
||||||
|
// SourceRecipe {
|
||||||
|
// name: "glibc-2.41".into(),
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_source(
|
||||||
|
// "bash-5.3",
|
||||||
|
// SourceRecipe {
|
||||||
|
// name: "bash-5.3".into(),
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_tool(
|
||||||
|
// "binutils",
|
||||||
|
// ToolRecipe {
|
||||||
|
// name: "binutils".into(),
|
||||||
|
// sources: vec!["binutils-2.46".into()],
|
||||||
|
// tools_wanted: vec![],
|
||||||
|
// pkgs_wanted: vec![],
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_tool(
|
||||||
|
// "gcc-bootstrap",
|
||||||
|
// ToolRecipe {
|
||||||
|
// name: "gcc-bootstrap".into(),
|
||||||
|
// sources: vec!["gcc-16.1.0".into()],
|
||||||
|
// tools_wanted: vec!["binutils".into()],
|
||||||
|
// pkgs_wanted: vec![],
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_tool(
|
||||||
|
// "gcc",
|
||||||
|
// ToolRecipe {
|
||||||
|
// name: "gcc".into(),
|
||||||
|
// sources: vec!["gcc-16.1.0".into()],
|
||||||
|
// tools_wanted: vec!["binutils".into()],
|
||||||
|
// pkgs_wanted: vec!["glibc".into()],
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_package(
|
||||||
|
// "linux-headers",
|
||||||
|
// PackageRecipe {
|
||||||
|
// name: "linux-headers".into(),
|
||||||
|
// sources: vec!["linux-7.0.9".into()],
|
||||||
|
// tools_wanted: vec![],
|
||||||
|
// pkgs_wanted: vec![],
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_package(
|
||||||
|
// "glibc",
|
||||||
|
// PackageRecipe {
|
||||||
|
// name: "glibc".into(),
|
||||||
|
// sources: vec!["glibc-2.41".into()],
|
||||||
|
// tools_wanted: vec!["gcc-bootstrap".into()],
|
||||||
|
// pkgs_wanted: vec!["linux-headers".into()],
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// recipes.add_package(
|
||||||
|
// "bash",
|
||||||
|
// PackageRecipe {
|
||||||
|
// name: "bash".into(),
|
||||||
|
// sources: vec!["bash-5.3".into()],
|
||||||
|
// tools_wanted: vec!["gcc".into()],
|
||||||
|
// pkgs_wanted: vec!["glibc".into()],
|
||||||
|
// },
|
||||||
|
// );
|
||||||
|
|
||||||
|
// let mut plan = Plan::new(&recipes);
|
||||||
|
|
||||||
|
// plan.add_wanted(PlanKey::PkgPackage(
|
||||||
|
// recipes.package("bash").expect("back package"),
|
||||||
|
// ));
|
||||||
|
|
||||||
|
// println!("{:#?}", plan.steps()?);
|
||||||
|
|
||||||
|
// Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
use allocative::Allocative;
|
|
||||||
use starlark::values::{Heap, OwnedFrozenValue, StarlarkValue, Value};
|
|
||||||
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
|
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub struct Options {
|
|
||||||
entries: HashMap<String, OwnedFrozenValue>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Options {
|
|
||||||
pub fn new(entries: HashMap<String, OwnedFrozenValue>) -> Self {
|
|
||||||
Self { entries }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Options {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "options")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark::starlark_simple_value!(Options);
|
|
||||||
|
|
||||||
#[starlark_value(type = "options")]
|
|
||||||
impl<'v> StarlarkValue<'v> for Options {
|
|
||||||
fn get_attr(&self, attribute: &str, _heap: &'v Heap) -> Option<Value<'v>> {
|
|
||||||
let owned = self.entries.get(attribute)?;
|
|
||||||
// SAFETY: `self` is kept alive by the module heap into which it was
|
|
||||||
// allocated, and `owned` holds an Arc to its source frozen heap. The
|
|
||||||
// returned Value therefore remains valid for as long as the receiving
|
|
||||||
// module is alive.
|
|
||||||
Some(unsafe { owned.unchecked_frozen_value() }.to_value())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-296
@@ -1,296 +0,0 @@
|
|||||||
use std::{cell::RefCell, collections::BTreeMap, rc::Rc};
|
|
||||||
|
|
||||||
use allocative::Allocative;
|
|
||||||
use anyhow::anyhow;
|
|
||||||
use starlark::{
|
|
||||||
collections::SmallMap,
|
|
||||||
environment::{Methods, MethodsBuilder, MethodsStatic, Module},
|
|
||||||
eval::Evaluator,
|
|
||||||
values::{Heap, OwnedFrozenValue, StarlarkValue, Value, list::UnpackList, none::NoneType},
|
|
||||||
};
|
|
||||||
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_module, starlark_value};
|
|
||||||
|
|
||||||
use crate::container::Container;
|
|
||||||
|
|
||||||
thread_local! {
|
|
||||||
static CURRENT: RefCell<Option<PhaseRuntime>> = const { RefCell::new(None) };
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
|
||||||
pub struct PhaseRuntime {
|
|
||||||
pub container: Rc<RefCell<Container>>,
|
|
||||||
pub base_path: String,
|
|
||||||
pub base_env: Vec<(String, String)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct PhaseRuntimeGuard;
|
|
||||||
|
|
||||||
impl PhaseRuntimeGuard {
|
|
||||||
pub fn enter(runtime: PhaseRuntime) -> Self {
|
|
||||||
CURRENT.with(|cell| {
|
|
||||||
let prev = cell.borrow_mut().replace(runtime);
|
|
||||||
assert!(prev.is_none(), "phase runtime already set");
|
|
||||||
});
|
|
||||||
Self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Drop for PhaseRuntimeGuard {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
CURRENT.with(|cell| {
|
|
||||||
cell.borrow_mut().take();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_current<R>(f: impl FnOnce(&PhaseRuntime) -> R) -> anyhow::Result<R> {
|
|
||||||
CURRENT.with(|cell| {
|
|
||||||
let borrow = cell.borrow();
|
|
||||||
let runtime = borrow
|
|
||||||
.as_ref()
|
|
||||||
.ok_or_else(|| anyhow!("ctx.run called outside of a phase invocation"))?;
|
|
||||||
Ok(f(runtime))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub struct SourceDir {
|
|
||||||
default: String,
|
|
||||||
entries: BTreeMap<String, String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SourceDir {
|
|
||||||
pub fn single(path: impl Into<String>) -> Self {
|
|
||||||
Self {
|
|
||||||
default: path.into(),
|
|
||||||
entries: BTreeMap::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn named<I, K, V>(entries: I) -> Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = (K, V)>,
|
|
||||||
K: Into<String>,
|
|
||||||
V: Into<String>,
|
|
||||||
{
|
|
||||||
let entries: BTreeMap<String, String> = entries
|
|
||||||
.into_iter()
|
|
||||||
.map(|(k, v)| (k.into(), v.into()))
|
|
||||||
.collect();
|
|
||||||
let default = entries
|
|
||||||
.values()
|
|
||||||
.next()
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| "/sources".to_owned());
|
|
||||||
Self { default, entries }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for SourceDir {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
f.write_str(&self.default)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark::starlark_simple_value!(SourceDir);
|
|
||||||
|
|
||||||
#[starlark_value(type = "source_dir")]
|
|
||||||
impl<'v> StarlarkValue<'v> for SourceDir {
|
|
||||||
fn at(&self, index: Value<'v>, heap: &'v Heap) -> starlark::Result<Value<'v>> {
|
|
||||||
let key = index.unpack_str().ok_or_else(|| {
|
|
||||||
starlark::Error::new_other(anyhow!("source_dir index must be a string"))
|
|
||||||
})?;
|
|
||||||
let path = self.entries.get(key).ok_or_else(|| {
|
|
||||||
starlark::Error::new_other(anyhow!(
|
|
||||||
"no source named `{key}` (available: {})",
|
|
||||||
if self.entries.is_empty() {
|
|
||||||
"<none>".to_owned()
|
|
||||||
} else {
|
|
||||||
self.entries
|
|
||||||
.keys()
|
|
||||||
.map(String::as_str)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
}
|
|
||||||
))
|
|
||||||
})?;
|
|
||||||
Ok(heap.alloc(path.as_str()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add(&self, rhs: Value<'v>, heap: &'v Heap) -> Option<starlark::Result<Value<'v>>> {
|
|
||||||
let suffix = rhs.unpack_str()?;
|
|
||||||
Some(Ok(heap.alloc(format!("{}{}", self.default, suffix))))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn radd(&self, lhs: Value<'v>, heap: &'v Heap) -> Option<starlark::Result<Value<'v>>> {
|
|
||||||
let prefix = lhs.unpack_str()?;
|
|
||||||
Some(Ok(heap.alloc(format!("{}{}", prefix, self.default))))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub struct PhaseContext {
|
|
||||||
source_dir: SourceDir,
|
|
||||||
build_dir: String,
|
|
||||||
prefix: String,
|
|
||||||
sysroot: String,
|
|
||||||
jobs: i32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PhaseContext {
|
|
||||||
pub fn new(source_dir: SourceDir, prefix: &str, jobs: i32) -> Self {
|
|
||||||
Self {
|
|
||||||
source_dir,
|
|
||||||
build_dir: "/build".to_owned(),
|
|
||||||
prefix: prefix.to_owned(),
|
|
||||||
sysroot: "/sysroot".to_owned(),
|
|
||||||
jobs,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for PhaseContext {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "ctx")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark::starlark_simple_value!(PhaseContext);
|
|
||||||
|
|
||||||
#[starlark_value(type = "phase_context")]
|
|
||||||
impl<'v> StarlarkValue<'v> for PhaseContext {
|
|
||||||
fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option<Value<'v>> {
|
|
||||||
Some(match attr {
|
|
||||||
"source_dir" => heap.alloc(self.source_dir.clone()),
|
|
||||||
"build_dir" => heap.alloc(self.build_dir.as_str()),
|
|
||||||
"prefix" => heap.alloc(self.prefix.as_str()),
|
|
||||||
"sysroot" => heap.alloc(self.sysroot.as_str()),
|
|
||||||
"jobs" => heap.alloc(self.jobs),
|
|
||||||
_ => return None,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_attr(&self, attr: &str, _heap: &'v Heap) -> bool {
|
|
||||||
matches!(
|
|
||||||
attr,
|
|
||||||
"source_dir" | "build_dir" | "prefix" | "sysroot" | "jobs" | "run"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dir_attr(&self) -> Vec<String> {
|
|
||||||
[
|
|
||||||
"source_dir",
|
|
||||||
"build_dir",
|
|
||||||
"prefix",
|
|
||||||
"sysroot",
|
|
||||||
"jobs",
|
|
||||||
"run",
|
|
||||||
]
|
|
||||||
.into_iter()
|
|
||||||
.map(String::from)
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_methods() -> Option<&'static Methods> {
|
|
||||||
static RES: MethodsStatic = MethodsStatic::new();
|
|
||||||
RES.methods(phase_context_methods)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[starlark_module]
|
|
||||||
fn phase_context_methods(builder: &mut MethodsBuilder) {
|
|
||||||
fn run<'v>(
|
|
||||||
#[starlark(this)] _this: Value<'v>,
|
|
||||||
#[starlark(require = pos)] argv: UnpackList<String>,
|
|
||||||
#[starlark(require = named)] env: Option<SmallMap<String, String>>,
|
|
||||||
) -> anyhow::Result<NoneType> {
|
|
||||||
run_in_container(&argv.items, env.unwrap_or_default())?;
|
|
||||||
Ok(NoneType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn run_in_container(
|
|
||||||
argv: &[String],
|
|
||||||
env_overrides: SmallMap<String, String>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let (container, env) = with_current(|runtime| {
|
|
||||||
let mut env: Vec<(String, String)> = runtime
|
|
||||||
.base_env
|
|
||||||
.iter()
|
|
||||||
.cloned()
|
|
||||||
.map(|(k, v)| {
|
|
||||||
if k == "PATH" {
|
|
||||||
(k, runtime.base_path.clone())
|
|
||||||
} else {
|
|
||||||
(k, v)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
for (k, v) in env_overrides {
|
|
||||||
if let Some(slot) = env.iter_mut().find(|(existing, _)| existing == &k) {
|
|
||||||
slot.1 = v;
|
|
||||||
} else {
|
|
||||||
env.push((k, v));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
(runtime.container.clone(), env)
|
|
||||||
})?;
|
|
||||||
container.borrow().exec(argv, &env, "/build")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub struct PackageContext {
|
|
||||||
dest_dir: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PackageContext {
|
|
||||||
pub fn new(dest_dir: String) -> Self {
|
|
||||||
Self { dest_dir }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for PackageContext {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "pkg")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark::starlark_simple_value!(PackageContext);
|
|
||||||
|
|
||||||
#[starlark_value(type = "package_context")]
|
|
||||||
impl<'v> StarlarkValue<'v> for PackageContext {
|
|
||||||
fn get_attr(&self, attr: &str, heap: &'v Heap) -> Option<Value<'v>> {
|
|
||||||
match attr {
|
|
||||||
"dest_dir" => Some(heap.alloc(self.dest_dir.as_str())),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn has_attr(&self, attr: &str, _heap: &'v Heap) -> bool {
|
|
||||||
attr == "dest_dir"
|
|
||||||
}
|
|
||||||
|
|
||||||
fn dir_attr(&self) -> Vec<String> {
|
|
||||||
vec!["dest_dir".to_owned()]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn invoke_phase(func: &OwnedFrozenValue, args: &[PhaseArg]) -> anyhow::Result<()> {
|
|
||||||
let module = Module::new();
|
|
||||||
let mut eval = Evaluator::new(&module);
|
|
||||||
let allocated: Vec<Value<'_>> = args
|
|
||||||
.iter()
|
|
||||||
.map(|arg| match arg {
|
|
||||||
PhaseArg::Ctx(ctx) => module.heap().alloc(ctx.clone()),
|
|
||||||
PhaseArg::Pkg(pkg) => module.heap().alloc(pkg.clone()),
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
eval.eval_function(func.value(), &allocated, &[])
|
|
||||||
.map_err(|err| anyhow!("{err}"))?;
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum PhaseArg {
|
|
||||||
Ctx(PhaseContext),
|
|
||||||
Pkg(PackageContext),
|
|
||||||
}
|
|
||||||
+222
@@ -0,0 +1,222 @@
|
|||||||
|
use crate::recipe::{PackageRecipe, RecipeSet, SourceRecipe, ToolRecipe};
|
||||||
|
|
||||||
|
use petgraph::{
|
||||||
|
Direction,
|
||||||
|
graph::{DiGraph, NodeIndex},
|
||||||
|
};
|
||||||
|
use smallvec::{SmallVec, smallvec};
|
||||||
|
use std::{
|
||||||
|
cmp::Reverse,
|
||||||
|
collections::{BinaryHeap, HashMap, HashSet},
|
||||||
|
};
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum PlanError {
|
||||||
|
#[error("missing source recipe '{0}'")]
|
||||||
|
MissingSource(String),
|
||||||
|
#[error("missing tool recipe '{0}'")]
|
||||||
|
MissingTool(String),
|
||||||
|
#[error("missing package recipe '{0}'")]
|
||||||
|
MissingPackage(String),
|
||||||
|
#[error("plan cycle detected")]
|
||||||
|
CycleDetected,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub enum PlanKey {
|
||||||
|
SourceFetch(String),
|
||||||
|
SourcePatch(String),
|
||||||
|
SourcePrepare(String),
|
||||||
|
|
||||||
|
ToolConfigure(String),
|
||||||
|
ToolBuild(String),
|
||||||
|
ToolInstall(String),
|
||||||
|
|
||||||
|
PkgConfigure(String),
|
||||||
|
PkgBuild(String),
|
||||||
|
PkgInstall(String),
|
||||||
|
PkgPackage(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlanKey {
|
||||||
|
fn weight(&self) -> i8 {
|
||||||
|
match self {
|
||||||
|
PlanKey::SourceFetch(_) => 0,
|
||||||
|
PlanKey::SourcePatch(_) => 1,
|
||||||
|
PlanKey::SourcePrepare(_) => 2,
|
||||||
|
PlanKey::ToolConfigure(_) => 3,
|
||||||
|
PlanKey::ToolBuild(_) => 4,
|
||||||
|
PlanKey::ToolInstall(_) => 5,
|
||||||
|
PlanKey::PkgConfigure(_) => 6,
|
||||||
|
PlanKey::PkgBuild(_) => 7,
|
||||||
|
PlanKey::PkgInstall(_) => 8,
|
||||||
|
PlanKey::PkgPackage(_) => 9,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dependencies(&self, recipes: &RecipeSet) -> Result<SmallVec<[PlanKey; 8]>, PlanError> {
|
||||||
|
match self {
|
||||||
|
PlanKey::SourceFetch(_) => Ok(smallvec![]),
|
||||||
|
PlanKey::SourcePatch(recipe) => Ok(smallvec![PlanKey::SourceFetch(recipe.clone())]),
|
||||||
|
PlanKey::SourcePrepare(recipe) => Ok(smallvec![PlanKey::SourcePatch(recipe.clone())]),
|
||||||
|
PlanKey::ToolConfigure(recipe) => {
|
||||||
|
let recipe = recipes
|
||||||
|
.tool(recipe)
|
||||||
|
.ok_or(PlanError::MissingTool(recipe.clone()))?;
|
||||||
|
|
||||||
|
let source_deps = recipe.sources.iter().map(|name| {
|
||||||
|
recipes
|
||||||
|
.source(name)
|
||||||
|
.map(|_| PlanKey::SourcePrepare(name.to_string()))
|
||||||
|
.ok_or(PlanError::MissingSource(name.to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let tool_deps = recipe.tools_wanted.iter().map(|name| {
|
||||||
|
recipes
|
||||||
|
.tool(name)
|
||||||
|
.map(|_| PlanKey::ToolInstall(name.to_string()))
|
||||||
|
.ok_or(PlanError::MissingTool(name.to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let pkg_deps = recipe.pkgs_wanted.iter().map(|name| {
|
||||||
|
recipes
|
||||||
|
.package(name)
|
||||||
|
.map(|_| PlanKey::PkgPackage(name.to_string()))
|
||||||
|
.ok_or(PlanError::MissingPackage(name.to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
source_deps.chain(tool_deps).chain(pkg_deps).collect()
|
||||||
|
}
|
||||||
|
PlanKey::ToolBuild(recipe) => Ok(smallvec![PlanKey::ToolConfigure(recipe.clone())]),
|
||||||
|
PlanKey::ToolInstall(recipe) => Ok(smallvec![PlanKey::ToolBuild(recipe.clone())]),
|
||||||
|
PlanKey::PkgConfigure(recipe) => {
|
||||||
|
let recipe = recipes
|
||||||
|
.package(recipe)
|
||||||
|
.ok_or(PlanError::MissingPackage(recipe.clone()))?;
|
||||||
|
|
||||||
|
let source_deps = recipe.sources.iter().map(|name| {
|
||||||
|
recipes
|
||||||
|
.source(name)
|
||||||
|
.map(|_| PlanKey::SourcePrepare(name.to_string()))
|
||||||
|
.ok_or(PlanError::MissingSource(name.to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let tool_deps = recipe.tools_wanted.iter().map(|name| {
|
||||||
|
recipes
|
||||||
|
.tool(name)
|
||||||
|
.map(|_| PlanKey::ToolInstall(name.to_string()))
|
||||||
|
.ok_or(PlanError::MissingTool(name.to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
let pkg_deps = recipe.pkgs_wanted.iter().map(|name| {
|
||||||
|
recipes
|
||||||
|
.package(name)
|
||||||
|
.map(|_| PlanKey::PkgPackage(name.to_string()))
|
||||||
|
.ok_or(PlanError::MissingPackage(name.to_string()))
|
||||||
|
});
|
||||||
|
|
||||||
|
source_deps.chain(tool_deps).chain(pkg_deps).collect()
|
||||||
|
}
|
||||||
|
PlanKey::PkgBuild(recipe) => Ok(smallvec![PlanKey::PkgConfigure(recipe.clone())]),
|
||||||
|
PlanKey::PkgInstall(recipe) => Ok(smallvec![PlanKey::PkgBuild(recipe.clone())]),
|
||||||
|
PlanKey::PkgPackage(recipe) => Ok(smallvec![PlanKey::PkgInstall(recipe.clone())]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_active(&self) -> bool {
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Plan<'a> {
|
||||||
|
recipes: &'a RecipeSet<'a>,
|
||||||
|
wanted: HashSet<PlanKey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Plan<'a> {
|
||||||
|
pub fn new(recipes: &'a RecipeSet) -> Self {
|
||||||
|
Self {
|
||||||
|
recipes,
|
||||||
|
wanted: HashSet::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_wanted(&mut self, key: PlanKey) {
|
||||||
|
self.wanted.insert(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn steps(&self) -> Result<Vec<PlanKey>, PlanError> {
|
||||||
|
let mut stack: Vec<_> = self.wanted.iter().cloned().collect();
|
||||||
|
let mut graph: DiGraph<_, ()> = DiGraph::new();
|
||||||
|
let mut nodes = HashMap::new();
|
||||||
|
|
||||||
|
while let Some(node) = stack.pop() {
|
||||||
|
let node_idx = match nodes.get(&node) {
|
||||||
|
Some(&idx) => idx,
|
||||||
|
None => {
|
||||||
|
let idx = graph.add_node(node.clone());
|
||||||
|
nodes.insert(node.clone(), idx);
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for dep in node.dependencies(self.recipes)? {
|
||||||
|
let dep_idx = match nodes.get(&dep) {
|
||||||
|
Some(&idx) => idx,
|
||||||
|
None => {
|
||||||
|
let idx = graph.add_node(dep.clone());
|
||||||
|
nodes.insert(dep.clone(), idx);
|
||||||
|
stack.push(dep);
|
||||||
|
idx
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
graph.update_edge(dep_idx, node_idx, ());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// petgraph::algo::toposort(&graph, None)
|
||||||
|
// .and_then(|nodes| {
|
||||||
|
// Ok(nodes
|
||||||
|
// .iter()
|
||||||
|
// .map(|&k| graph[k])
|
||||||
|
// .filter(|node| node.is_active())
|
||||||
|
// .collect())
|
||||||
|
// })
|
||||||
|
// .map_err(|_| PlanError::CycleDetected)
|
||||||
|
|
||||||
|
let mut in_degree: HashMap<NodeIndex, usize> = graph
|
||||||
|
.node_indices()
|
||||||
|
.map(|i| (i, graph.neighbors_directed(i, Direction::Incoming).count()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut heap: BinaryHeap<(Reverse<i8>, NodeIndex)> = in_degree
|
||||||
|
.iter()
|
||||||
|
.filter(|&(_, d)| *d == 0)
|
||||||
|
.map(|(&i, _)| (Reverse(graph[i].weight()), i))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let mut result = Vec::with_capacity(graph.node_count());
|
||||||
|
|
||||||
|
while let Some((_, idx)) = heap.pop() {
|
||||||
|
result.push(graph[idx].clone());
|
||||||
|
|
||||||
|
for neighbor in graph.neighbors_directed(idx, Direction::Outgoing) {
|
||||||
|
let d = in_degree.get_mut(&neighbor).unwrap();
|
||||||
|
*d -= 1;
|
||||||
|
if *d == 0 {
|
||||||
|
heap.push((Reverse(graph[neighbor].weight()), neighbor));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if result.len() != graph.node_count() {
|
||||||
|
Err(PlanError::CycleDetected)
|
||||||
|
} else {
|
||||||
|
result.retain(|node| node.is_active());
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+256
@@ -0,0 +1,256 @@
|
|||||||
|
use anyhow::Context;
|
||||||
|
use starlark::{
|
||||||
|
environment::{FrozenModule, GlobalsBuilder, Module},
|
||||||
|
eval,
|
||||||
|
values::{
|
||||||
|
UnpackValue,
|
||||||
|
typing::{FrozenStarlarkCallable, StarlarkCallable, StarlarkCallableParamSpec},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use crate::eval::{
|
||||||
|
Config, Metadata, TarballSource, UnpackCloned, eval_files, recipe_globals, types_globals,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SourceRecipe {
|
||||||
|
pub name: String,
|
||||||
|
pub source: Box<dyn Source>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait Source {}
|
||||||
|
|
||||||
|
pub struct ToolRecipe {
|
||||||
|
pub name: String,
|
||||||
|
pub sources: Vec<String>,
|
||||||
|
pub tools_wanted: Vec<String>,
|
||||||
|
pub pkgs_wanted: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct PackageRecipe {
|
||||||
|
pub name: String,
|
||||||
|
pub meta: Option<Metadata>,
|
||||||
|
pub version: String,
|
||||||
|
pub revision: u32,
|
||||||
|
pub sources: Vec<String>,
|
||||||
|
pub tools_wanted: Vec<String>,
|
||||||
|
pub pkgs_wanted: Vec<String>,
|
||||||
|
pub module: FrozenModule,
|
||||||
|
pub configure: Option<FrozenStarlarkCallable>,
|
||||||
|
pub build: Option<FrozenStarlarkCallable>,
|
||||||
|
pub install: Option<FrozenStarlarkCallable>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct RecipeSet<'a> {
|
||||||
|
sources: HashMap<String, SourceRecipe>,
|
||||||
|
tools: HashMap<String, ToolRecipe>,
|
||||||
|
pub packages: HashMap<String, PackageRecipe>,
|
||||||
|
config: &'a Config,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> RecipeSet<'a> {
|
||||||
|
fn add_source(&mut self, name: &str, recipe: SourceRecipe) -> anyhow::Result<()> {
|
||||||
|
if self.sources.insert(name.to_string(), recipe).is_some() {
|
||||||
|
anyhow::bail!("source '{name}' already exists");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_tool(&mut self, name: &str, recipe: ToolRecipe) -> anyhow::Result<()> {
|
||||||
|
if self.tools.insert(name.to_string(), recipe).is_some() {
|
||||||
|
anyhow::bail!("tool '{name}' already exists");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_package(&mut self, name: &str, recipe: PackageRecipe) -> anyhow::Result<()> {
|
||||||
|
if self.packages.insert(name.to_string(), recipe).is_some() {
|
||||||
|
anyhow::bail!("package '{name}' already exists");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_tool_recipe(&mut self, name: &str, path: &Path) -> anyhow::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_package_recipe(&mut self, name: &str, path: &Path) -> anyhow::Result<()> {
|
||||||
|
let module = eval_files(
|
||||||
|
&[path],
|
||||||
|
&GlobalsBuilder::standard()
|
||||||
|
.with(types_globals)
|
||||||
|
.with(recipe_globals)
|
||||||
|
.build(),
|
||||||
|
None,
|
||||||
|
Some(self.config),
|
||||||
|
None,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let module = module.freeze().map_err(|err| anyhow::anyhow!("{err:?}"))?;
|
||||||
|
|
||||||
|
let version: String = get_value(&module, "version")?;
|
||||||
|
let revision: u32 = get_value_option(&module, "revision")?.unwrap_or(1);
|
||||||
|
let metadata: Option<Metadata> = get_value_option(&module, "metadata")?;
|
||||||
|
let source: TarballSource = get_value(&module, "source")?;
|
||||||
|
|
||||||
|
let configure = get_frozen_callable(&module, "configure")?;
|
||||||
|
let build = get_frozen_callable(&module, "build")?;
|
||||||
|
let install = get_frozen_callable(&module, "install")?;
|
||||||
|
|
||||||
|
self.add_source(
|
||||||
|
name,
|
||||||
|
SourceRecipe {
|
||||||
|
name: name.to_string(),
|
||||||
|
source: Box::new(source),
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
|
||||||
|
self.add_package(
|
||||||
|
name,
|
||||||
|
PackageRecipe {
|
||||||
|
name: name.to_string(),
|
||||||
|
meta: metadata,
|
||||||
|
version,
|
||||||
|
revision,
|
||||||
|
sources: vec![name.to_string()],
|
||||||
|
tools_wanted: vec![],
|
||||||
|
pkgs_wanted: vec![],
|
||||||
|
module,
|
||||||
|
configure,
|
||||||
|
build,
|
||||||
|
install,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(config: &'a Config) -> Self {
|
||||||
|
Self {
|
||||||
|
sources: HashMap::new(),
|
||||||
|
tools: HashMap::new(),
|
||||||
|
packages: HashMap::new(),
|
||||||
|
config,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_recipes(
|
||||||
|
&mut self,
|
||||||
|
recipes_dir: &Path,
|
||||||
|
host_recipes_dir: &Path,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
for (dir, tool_recipe) in [(recipes_dir, false), (host_recipes_dir, true)] {
|
||||||
|
for entry in std::fs::read_dir(dir)? {
|
||||||
|
let entry = entry.context("reading directory entry")?;
|
||||||
|
|
||||||
|
if let Some((name, path)) = get_recipe_name_and_patch(&entry)? {
|
||||||
|
if tool_recipe {
|
||||||
|
self.load_tool_recipe(&name, &path)?;
|
||||||
|
} else {
|
||||||
|
self.load_package_recipe(&name, &path)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn source(&self, name: &str) -> Option<&SourceRecipe> {
|
||||||
|
self.sources.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn tool(&self, name: &str) -> Option<&ToolRecipe> {
|
||||||
|
self.tools.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn package(&self, name: &str) -> Option<&PackageRecipe> {
|
||||||
|
self.packages.get(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_recipe_name_and_patch(
|
||||||
|
entry: &std::fs::DirEntry,
|
||||||
|
) -> anyhow::Result<Option<(String, PathBuf)>> {
|
||||||
|
let file_type = entry.file_type()?;
|
||||||
|
let path = entry.path();
|
||||||
|
|
||||||
|
if file_type.is_dir() {
|
||||||
|
let recipe_path = path.join("recipe.star");
|
||||||
|
|
||||||
|
if recipe_path.exists() {
|
||||||
|
return Ok(Some((
|
||||||
|
entry.file_name().to_str().unwrap_or("").to_string(),
|
||||||
|
recipe_path,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let name = path.file_stem().unwrap().to_str().unwrap_or("").to_string();
|
||||||
|
let extension = path
|
||||||
|
.extension()
|
||||||
|
.ok_or(anyhow::anyhow!("File did not have an extension"))?;
|
||||||
|
|
||||||
|
if extension == "star" {
|
||||||
|
return Ok(Some((name, path)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_value_option<T: UnpackCloned>(
|
||||||
|
module: &FrozenModule,
|
||||||
|
name: &str,
|
||||||
|
) -> anyhow::Result<Option<T>> {
|
||||||
|
module
|
||||||
|
.get_option(name)?
|
||||||
|
.map(|value| {
|
||||||
|
T::unpack_cloned(value.value()).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"`{name}` should be of type `{}` but got `{}`",
|
||||||
|
T::starlark_type_repr(),
|
||||||
|
value.value().get_type()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.transpose()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_value<T: UnpackCloned>(module: &FrozenModule, name: &str) -> anyhow::Result<T> {
|
||||||
|
let value = module
|
||||||
|
.get_option(name)?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("`{name}` is required"))?;
|
||||||
|
|
||||||
|
T::unpack_cloned(value.value()).ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"`{name}` should be of type `{}` but got `{}`",
|
||||||
|
T::starlark_type_repr(),
|
||||||
|
value.value().get_type()
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_frozen_callable<P: StarlarkCallableParamSpec>(
|
||||||
|
module: &FrozenModule,
|
||||||
|
name: &str,
|
||||||
|
) -> anyhow::Result<Option<FrozenStarlarkCallable<P>>> {
|
||||||
|
let Some(value) = module.get_option(name)? else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let callable = StarlarkCallable::unpack_value(value.value())
|
||||||
|
.map_err(|err| anyhow::anyhow!("{err}"))?
|
||||||
|
.ok_or_else(|| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"`{name}` should be callable but got `{}`",
|
||||||
|
value.value().get_type()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
callable
|
||||||
|
.unpack_frozen()
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("`{name}` was callable but not frozen"))
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
use allocative::Allocative;
|
|
||||||
use starlark::values::StarlarkValue;
|
|
||||||
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub struct Metadata {
|
|
||||||
maintainer: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
license: Option<String>,
|
|
||||||
website: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Metadata {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "metadata")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark::starlark_simple_value!(Metadata);
|
|
||||||
|
|
||||||
#[starlark_value(type = "metadata")]
|
|
||||||
impl<'v> StarlarkValue<'v> for Metadata {}
|
|
||||||
|
|
||||||
impl Metadata {
|
|
||||||
pub fn new(
|
|
||||||
maintainer: Option<String>,
|
|
||||||
description: Option<String>,
|
|
||||||
license: Option<String>,
|
|
||||||
website: Option<String>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
maintainer,
|
|
||||||
description,
|
|
||||||
license,
|
|
||||||
website,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn maintainer(&self) -> Option<&str> {
|
|
||||||
self.maintainer.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn description(&self) -> Option<&str> {
|
|
||||||
self.description.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn license(&self) -> Option<&str> {
|
|
||||||
self.license.as_deref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn website(&self) -> Option<&str> {
|
|
||||||
self.website.as_deref()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,532 +0,0 @@
|
|||||||
mod metadata;
|
|
||||||
mod source;
|
|
||||||
mod subpackage;
|
|
||||||
|
|
||||||
use anyhow::{Context, bail};
|
|
||||||
use starlark::{
|
|
||||||
environment::{FrozenModule, Module},
|
|
||||||
values::{
|
|
||||||
OwnedFrozenValue, UnpackValue, ValueLike, dict::DictRef, list::ListRef,
|
|
||||||
typing::StarlarkCallable,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
use std::{
|
|
||||||
collections::HashMap,
|
|
||||||
fmt,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
};
|
|
||||||
use walkdir::WalkDir;
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
eval::{self, ExtractError},
|
|
||||||
options::Options,
|
|
||||||
};
|
|
||||||
|
|
||||||
pub use metadata::Metadata;
|
|
||||||
pub use source::{GitSource, Source, TarballSource};
|
|
||||||
pub use subpackage::Subpackage;
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
||||||
pub enum RecipeKind {
|
|
||||||
Package,
|
|
||||||
HostPackage,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecipeKind {
|
|
||||||
pub fn key(self, name: &str) -> String {
|
|
||||||
match self {
|
|
||||||
Self::Package => name.to_string(),
|
|
||||||
Self::HostPackage => format!("host:{name}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn slug(self, name: &str) -> String {
|
|
||||||
self.key(name).replace(':', "-")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Sources {
|
|
||||||
Single(source::Source),
|
|
||||||
Multiple(HashMap<String, source::Source>),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Sources {
|
|
||||||
pub fn entries(&self) -> Vec<(Option<&str>, &source::Source)> {
|
|
||||||
match self {
|
|
||||||
Self::Single(source) => vec![(None, source)],
|
|
||||||
Self::Multiple(sources) => {
|
|
||||||
let mut entries = sources
|
|
||||||
.iter()
|
|
||||||
.map(|(name, source)| (Some(name.as_str()), source))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
entries.sort_by_key(|(name, _)| *name);
|
|
||||||
entries
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Recipe {
|
|
||||||
/// Recipe name without namespace prefix.
|
|
||||||
name: String,
|
|
||||||
/// Path to the recipe's .star file.
|
|
||||||
path: PathBuf,
|
|
||||||
/// What kind of a recipe is that?
|
|
||||||
kind: RecipeKind,
|
|
||||||
/// Version shared by every package output of this recipe.
|
|
||||||
version: String,
|
|
||||||
/// Revision shared by every package output of this recipe.
|
|
||||||
revision: i32,
|
|
||||||
/// List of sources required to build this recipe.
|
|
||||||
sources: Sources,
|
|
||||||
/// All packages produced by this recipe.
|
|
||||||
/// This is empty for host recipes.
|
|
||||||
outputs: Vec<OutputPackage>,
|
|
||||||
/// Host packages requires for this recipe.
|
|
||||||
host_deps: Vec<String>,
|
|
||||||
/// Packages installed to the system root during build of the recipe, but
|
|
||||||
/// not listed as part of the `apk` dependencies.
|
|
||||||
build_deps: Vec<String>,
|
|
||||||
/// Packages installed to the system root during build of the recipe AND
|
|
||||||
/// listed as part of the `apk` dependencies.
|
|
||||||
deps: Vec<String>,
|
|
||||||
/// Packages NOT installed to the system root during build, only listed
|
|
||||||
/// as part of the `apk` dependencies.
|
|
||||||
run_deps: Vec<String>,
|
|
||||||
/// Starlark phase functions defined by the recipe.
|
|
||||||
phases: RecipePhases,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Recipe {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("Recipe")
|
|
||||||
.field("path", &self.path)
|
|
||||||
.field("kind", &self.kind)
|
|
||||||
.field("version", &self.version)
|
|
||||||
.field("revision", &self.revision)
|
|
||||||
.field("sources", &self.sources)
|
|
||||||
.field("outputs", &self.outputs)
|
|
||||||
.field("host_deps", &self.host_deps)
|
|
||||||
.field("build_deps", &self.build_deps)
|
|
||||||
.field("deps", &self.deps)
|
|
||||||
.field("run_deps", &self.run_deps)
|
|
||||||
.field("phases", &self.phases)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Recipe {
|
|
||||||
pub fn load(
|
|
||||||
path: &Path,
|
|
||||||
name: &str,
|
|
||||||
kind: RecipeKind,
|
|
||||||
options: &Options,
|
|
||||||
lib: Option<&FrozenModule>,
|
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
let module = eval::eval_file(path, Some(options), lib)
|
|
||||||
.with_context(|| format!("evaluating recipe {}", path.display()))?;
|
|
||||||
|
|
||||||
let version = eval::extract_string(&module, "version")
|
|
||||||
.map_err(|e| anyhow::anyhow!("field `version`: {e}"))?;
|
|
||||||
|
|
||||||
let revision = match eval::extract_i32(&module, "revision") {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(ExtractError::NotFound) => 1,
|
|
||||||
Err(e) => bail!("field `revision`: {e}"),
|
|
||||||
};
|
|
||||||
|
|
||||||
let metadata = match module.get("metadata") {
|
|
||||||
None => Metadata::new(None, None, None, None),
|
|
||||||
Some(value) => value
|
|
||||||
.downcast_ref::<Metadata>()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("field `metadata`: expected a metadata value"))?
|
|
||||||
.clone(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let source_value = module.get("source");
|
|
||||||
let sources_value = module.get("sources");
|
|
||||||
let sources = match (source_value, sources_value) {
|
|
||||||
(None, None) => bail!("recipe must define either `source` or `sources`"),
|
|
||||||
(Some(_), Some(_)) => {
|
|
||||||
bail!("recipe must define exactly one of `source` or `sources`, not both")
|
|
||||||
}
|
|
||||||
(Some(value), None) => {
|
|
||||||
let source = value
|
|
||||||
.downcast_ref::<Source>()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("field `source`: expected a source value"))?
|
|
||||||
.clone();
|
|
||||||
Sources::Single(source)
|
|
||||||
}
|
|
||||||
(None, Some(value)) => {
|
|
||||||
let dict = DictRef::from_value(value).ok_or_else(|| {
|
|
||||||
anyhow::anyhow!("field `sources`: expected a dict of name -> source")
|
|
||||||
})?;
|
|
||||||
if dict.iter().len() == 0 {
|
|
||||||
bail!("field `sources`: must contain at least one entry");
|
|
||||||
}
|
|
||||||
let mut map: HashMap<String, source::Source> = HashMap::new();
|
|
||||||
for (key, value) in dict.iter() {
|
|
||||||
let key_str = key
|
|
||||||
.unpack_str()
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("field `sources`: keys must be strings"))?;
|
|
||||||
if !is_valid_source_name(key_str) {
|
|
||||||
bail!(
|
|
||||||
"field `sources`: invalid source name `{key_str}` (allowed: letters, digits, `-`, `_`; must start with a letter or digit)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let source = value.downcast_ref::<Source>().ok_or_else(|| {
|
|
||||||
anyhow::anyhow!("field `sources`: entry `{key_str}` is not a source value")
|
|
||||||
})?;
|
|
||||||
if map.insert(key_str.to_owned(), source.clone()).is_some() {
|
|
||||||
bail!("field `sources`: duplicate key `{key_str}`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Sources::Multiple(map)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let host_deps = optional_string_list(&module, "host_deps")?;
|
|
||||||
let build_deps = optional_string_list(&module, "build_deps")?;
|
|
||||||
let deps = optional_string_list(&module, "deps")?;
|
|
||||||
let run_deps = optional_string_list(&module, "run_deps")?;
|
|
||||||
|
|
||||||
let recipe_key = kind.key(name);
|
|
||||||
let outputs = match kind {
|
|
||||||
RecipeKind::Package => {
|
|
||||||
let mut outputs = vec![OutputPackage {
|
|
||||||
recipe: recipe_key.clone(),
|
|
||||||
name: name.to_owned(),
|
|
||||||
metadata: metadata.clone(),
|
|
||||||
}];
|
|
||||||
if let Some(value) = module.get("subpackages") {
|
|
||||||
let list = ListRef::from_value(value)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("field `subpackages`: expected a list"))?;
|
|
||||||
for item in list.iter() {
|
|
||||||
let sub = item.downcast_ref::<Subpackage>().ok_or_else(|| {
|
|
||||||
anyhow::anyhow!(
|
|
||||||
"field `subpackages`: each entry must be a subpackage value"
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
outputs.push(OutputPackage {
|
|
||||||
recipe: recipe_key.clone(),
|
|
||||||
name: sub.name().to_owned(),
|
|
||||||
metadata: sub.metadata().clone(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
outputs
|
|
||||||
}
|
|
||||||
RecipeKind::HostPackage => {
|
|
||||||
if module.get("subpackages").is_some() {
|
|
||||||
bail!("host recipes cannot declare `subpackages`");
|
|
||||||
}
|
|
||||||
Vec::new()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let module = module
|
|
||||||
.freeze()
|
|
||||||
.map_err(|err| anyhow::anyhow!("freezing recipe module {}: {err:?}", path.display()))?;
|
|
||||||
let phases = RecipePhases::load(&module)?;
|
|
||||||
|
|
||||||
Ok(Recipe {
|
|
||||||
name: name.to_owned(),
|
|
||||||
path: path.to_path_buf(),
|
|
||||||
kind,
|
|
||||||
version,
|
|
||||||
revision,
|
|
||||||
sources,
|
|
||||||
outputs,
|
|
||||||
host_deps,
|
|
||||||
build_deps,
|
|
||||||
deps,
|
|
||||||
run_deps,
|
|
||||||
phases,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn phases(&self) -> &RecipePhases {
|
|
||||||
&self.phases
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn key(&self) -> String {
|
|
||||||
self.kind.key(&self.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn slug(&self) -> String {
|
|
||||||
self.kind.slug(&self.name)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn dir(&self) -> &Path {
|
|
||||||
self.path.parent().unwrap_or_else(|| Path::new("."))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn path(&self) -> &Path {
|
|
||||||
&self.path
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn kind(&self) -> RecipeKind {
|
|
||||||
self.kind
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn version(&self) -> &str {
|
|
||||||
&self.version
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn revision(&self) -> i32 {
|
|
||||||
self.revision
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sources(&self) -> &Sources {
|
|
||||||
&self.sources
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn outputs(&self) -> &[OutputPackage] {
|
|
||||||
&self.outputs
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn host_deps(&self) -> &[String] {
|
|
||||||
&self.host_deps
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build_deps(&self) -> &[String] {
|
|
||||||
&self.build_deps
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn deps(&self) -> &[String] {
|
|
||||||
&self.deps
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn run_deps(&self) -> &[String] {
|
|
||||||
&self.run_deps
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn optional_string_list(module: &Module, key: &str) -> anyhow::Result<Vec<String>> {
|
|
||||||
match eval::extract_string_list(module, key) {
|
|
||||||
Ok(v) => Ok(v),
|
|
||||||
Err(ExtractError::NotFound) => Ok(Vec::new()),
|
|
||||||
Err(e) => Err(anyhow::anyhow!("field `{key}`: {e}")),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn is_valid_source_name(name: &str) -> bool {
|
|
||||||
let mut chars = name.chars();
|
|
||||||
let Some(first) = chars.next() else {
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
if !first.is_ascii_alphanumeric() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
chars.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct RecipePhases {
|
|
||||||
configure: Option<OwnedFrozenValue>,
|
|
||||||
build: OwnedFrozenValue,
|
|
||||||
install: OwnedFrozenValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for RecipePhases {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("RecipePhases")
|
|
||||||
.field("configure", &self.configure.is_some())
|
|
||||||
.field("build", &true)
|
|
||||||
.field("install", &true)
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecipePhases {
|
|
||||||
fn load(module: &FrozenModule) -> anyhow::Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
configure: optional_phase_function(module, "configure")?,
|
|
||||||
build: required_phase_function(module, "build")?,
|
|
||||||
install: required_phase_function(module, "install")?,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn configure(&self) -> Option<&OwnedFrozenValue> {
|
|
||||||
self.configure.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn build(&self) -> &OwnedFrozenValue {
|
|
||||||
&self.build
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn install(&self) -> &OwnedFrozenValue {
|
|
||||||
&self.install
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn optional_phase_function(
|
|
||||||
module: &FrozenModule,
|
|
||||||
name: &str,
|
|
||||||
) -> anyhow::Result<Option<OwnedFrozenValue>> {
|
|
||||||
let Some(value) = module
|
|
||||||
.get_option(name)
|
|
||||||
.with_context(|| format!("field `{name}`"))?
|
|
||||||
else {
|
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
validate_phase_function(name, &value)?;
|
|
||||||
Ok(Some(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn required_phase_function(module: &FrozenModule, name: &str) -> anyhow::Result<OwnedFrozenValue> {
|
|
||||||
let value = module
|
|
||||||
.get_option(name)
|
|
||||||
.with_context(|| format!("field `{name}`"))?
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("field `{name}`: missing"))?;
|
|
||||||
|
|
||||||
validate_phase_function(name, &value)?;
|
|
||||||
Ok(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validate_phase_function(name: &str, value: &OwnedFrozenValue) -> anyhow::Result<()> {
|
|
||||||
let callable: Option<StarlarkCallable<'_>> = StarlarkCallable::unpack_value_opt(value.value());
|
|
||||||
if callable.is_none() {
|
|
||||||
bail!("field `{name}`: expected a callable value");
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct OutputPackage {
|
|
||||||
/// Canonical key of the owning recipe.
|
|
||||||
recipe: String,
|
|
||||||
/// Name of the output package.
|
|
||||||
name: String,
|
|
||||||
/// Metadata attached to the output package.
|
|
||||||
metadata: Metadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OutputPackage {
|
|
||||||
pub fn key(&self) -> String {
|
|
||||||
self.name.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recipe(&self) -> &str {
|
|
||||||
&self.recipe
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn metadata(&self) -> &Metadata {
|
|
||||||
&self.metadata
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct RecipeSet {
|
|
||||||
recipes: HashMap<String, Recipe>,
|
|
||||||
outputs: HashMap<String, OutputPackage>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RecipeSet {
|
|
||||||
pub fn load(
|
|
||||||
root_path: &Path,
|
|
||||||
options: &Options,
|
|
||||||
lib: Option<&FrozenModule>,
|
|
||||||
) -> anyhow::Result<Self> {
|
|
||||||
let mut recipes = HashMap::new();
|
|
||||||
let mut outputs = HashMap::new();
|
|
||||||
|
|
||||||
for (path, kind) in [
|
|
||||||
("recipes", RecipeKind::Package),
|
|
||||||
("host-recipes", RecipeKind::HostPackage),
|
|
||||||
] {
|
|
||||||
let recipes_dir = root_path.join(path);
|
|
||||||
|
|
||||||
if !recipes_dir.exists() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name, path) in discover_recipes(&recipes_dir)? {
|
|
||||||
let recipe = Recipe::load(&path, &name, kind, options, lib)
|
|
||||||
.with_context(|| format!("loading recipe `{name}`"))?;
|
|
||||||
let key = kind.key(&name);
|
|
||||||
|
|
||||||
if recipes.insert(key.clone(), recipe).is_some() {
|
|
||||||
bail!("duplicate recipe `{key}`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for recipe in recipes.values() {
|
|
||||||
for output in &recipe.outputs {
|
|
||||||
let key = recipe.kind.key(&output.name);
|
|
||||||
|
|
||||||
if outputs.insert(key.clone(), output.clone()).is_some() {
|
|
||||||
bail!("duplicate package output `{key}`");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self { recipes, outputs })
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn recipe(&self, key: &str) -> anyhow::Result<&Recipe> {
|
|
||||||
self.recipes
|
|
||||||
.get(key)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("unknown recipe `{key}`"))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn output(&self, key: &str) -> anyhow::Result<&OutputPackage> {
|
|
||||||
self.outputs
|
|
||||||
.get(key)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("unknown output package `{key}`"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find all recipe `.star` files under `dir`, returning a map of recipe name
|
|
||||||
/// to its `.star` file. Intermediate directories act as categories and are
|
|
||||||
/// not themselves recipes. Within any subtree, a recipe takes either form:
|
|
||||||
/// - `.../<name>.star` — name is the file stem
|
|
||||||
/// - `.../<name>/recipe.star` — name is the parent directory
|
|
||||||
fn discover_recipes(dir: &Path) -> anyhow::Result<HashMap<String, PathBuf>> {
|
|
||||||
let mut recipes: HashMap<String, PathBuf> = HashMap::new();
|
|
||||||
|
|
||||||
let walker = WalkDir::new(dir).follow_links(false);
|
|
||||||
for entry in walker {
|
|
||||||
let entry =
|
|
||||||
entry.with_context(|| format!("walking recipes directory {}", dir.display()))?;
|
|
||||||
if !entry.file_type().is_file() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let path = entry.path();
|
|
||||||
let Some(file_name) = path.file_name().and_then(|n| n.to_str()) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
let name = if file_name == "recipe.star" {
|
|
||||||
let Some(parent_name) = path
|
|
||||||
.parent()
|
|
||||||
.and_then(|p| p.file_name())
|
|
||||||
.and_then(|n| n.to_str())
|
|
||||||
else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
parent_name.to_owned()
|
|
||||||
} else if let Some(stem) = file_name.strip_suffix(".star") {
|
|
||||||
stem.to_owned()
|
|
||||||
} else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(existing) = recipes.insert(name.clone(), path.to_path_buf()) {
|
|
||||||
bail!(
|
|
||||||
"recipe `{name}` is defined twice: {} and {}",
|
|
||||||
existing.display(),
|
|
||||||
recipes[&name].display(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(recipes)
|
|
||||||
}
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
use allocative::Allocative;
|
|
||||||
use starlark::values::StarlarkValue;
|
|
||||||
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub struct TarballSource {
|
|
||||||
url: String,
|
|
||||||
sha256: String,
|
|
||||||
strip_components: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for TarballSource {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "tarball_source")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark::starlark_simple_value!(TarballSource);
|
|
||||||
|
|
||||||
#[starlark_value(type = "tarball_source")]
|
|
||||||
impl<'v> StarlarkValue<'v> for TarballSource {}
|
|
||||||
|
|
||||||
impl TarballSource {
|
|
||||||
pub fn new(url: String, sha256: String, strip_components: u32) -> Self {
|
|
||||||
Self {
|
|
||||||
url,
|
|
||||||
sha256,
|
|
||||||
strip_components,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn url(&self) -> &str {
|
|
||||||
&self.url
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn sha256(&self) -> &str {
|
|
||||||
&self.sha256
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn strip_components(&self) -> u32 {
|
|
||||||
self.strip_components
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub struct GitSource {
|
|
||||||
url: String,
|
|
||||||
commit: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for GitSource {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "git_source")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark::starlark_simple_value!(GitSource);
|
|
||||||
|
|
||||||
#[starlark_value(type = "git_source")]
|
|
||||||
impl<'v> StarlarkValue<'v> for GitSource {}
|
|
||||||
|
|
||||||
impl GitSource {
|
|
||||||
pub fn new(url: String, commit: String) -> Self {
|
|
||||||
Self { url, commit }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn url(&self) -> &str {
|
|
||||||
&self.url
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn commit(&self) -> &str {
|
|
||||||
&self.commit
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub enum Source {
|
|
||||||
Tarball(TarballSource),
|
|
||||||
Git(GitSource),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Source {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "source")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark::starlark_simple_value!(Source);
|
|
||||||
|
|
||||||
#[starlark_value(type = "source")]
|
|
||||||
impl<'v> StarlarkValue<'v> for Source {}
|
|
||||||
|
|
||||||
impl Source {
|
|
||||||
pub fn url(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Tarball(source) => source.url(),
|
|
||||||
Self::Git(source) => source.url(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn cache_key(&self) -> &str {
|
|
||||||
match self {
|
|
||||||
Self::Tarball(source) => source.sha256(),
|
|
||||||
Self::Git(source) => source.commit(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_unknown_cache_key(&self) -> bool {
|
|
||||||
matches!(self.cache_key(), "?" | "???")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
use allocative::Allocative;
|
|
||||||
use starlark::values::StarlarkValue;
|
|
||||||
use starlark_derive::{NoSerialize, ProvidesStaticType, starlark_value};
|
|
||||||
|
|
||||||
use crate::recipe::Metadata;
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Allocative, ProvidesStaticType, NoSerialize)]
|
|
||||||
pub struct Subpackage {
|
|
||||||
name: String,
|
|
||||||
metadata: Metadata,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Subpackage {
|
|
||||||
pub fn new(name: String, metadata: Metadata) -> Self {
|
|
||||||
Self { name, metadata }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn name(&self) -> &str {
|
|
||||||
&self.name
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn metadata(&self) -> &Metadata {
|
|
||||||
&self.metadata
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for Subpackage {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "subpackage")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
starlark::starlark_simple_value!(Subpackage);
|
|
||||||
|
|
||||||
#[starlark_value(type = "subpackage")]
|
|
||||||
impl<'v> StarlarkValue<'v> for Subpackage {}
|
|
||||||
Reference in New Issue
Block a user