wip 2
This commit is contained in:
@@ -1,23 +1,22 @@
|
|||||||
name = "gcc"
|
|
||||||
version = "16.1.0"
|
version = "16.1.0"
|
||||||
revision = 1
|
revision = 1
|
||||||
description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)"
|
metadata = meta(
|
||||||
license = "GPL-3.0-or-later"
|
description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)",
|
||||||
|
license = "GPL-3.0-or-later",
|
||||||
source = {
|
)
|
||||||
"url": f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
|
source = tarball_source(
|
||||||
"sha256": "50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
|
url = f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
|
||||||
"strip_components": 1,
|
sha256 = "50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
|
||||||
}
|
strip_components = 1,
|
||||||
|
)
|
||||||
host_deps = ["binutils"]
|
host_deps = ["binutils"]
|
||||||
|
|
||||||
def configure(ctx):
|
def configure(ctx):
|
||||||
ctx.run([
|
ctx.run([
|
||||||
ctx.source_dir + "/configure",
|
ctx.source_dir + "/configure",
|
||||||
|
"--target=" + options.target_triple,
|
||||||
"--prefix=" + ctx.prefix,
|
"--prefix=" + ctx.prefix,
|
||||||
"--target=" + OPTIONS.target_triple,
|
"--with-sysroot=" + ctx.sysroot,
|
||||||
"--with-sysroot=" + ctx.prefix + "/" + OPTIONS.target_triple,
|
|
||||||
"--without-headers",
|
"--without-headers",
|
||||||
"--with-newlib",
|
"--with-newlib",
|
||||||
"--enable-languages=c,c++",
|
"--enable-languages=c,c++",
|
||||||
@@ -33,9 +32,9 @@ def configure(ctx):
|
|||||||
"--disable-libvtv",
|
"--disable-libvtv",
|
||||||
"--disable-multilib",
|
"--disable-multilib",
|
||||||
], env = {
|
], 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,
|
||||||
})
|
})
|
||||||
|
|
||||||
def build(ctx):
|
def build(ctx):
|
||||||
@@ -44,5 +43,5 @@ def build(ctx):
|
|||||||
ctx.run(["make", jobs, "all-target-libgcc"])
|
ctx.run(["make", jobs, "all-target-libgcc"])
|
||||||
|
|
||||||
def install(ctx, pkg):
|
def install(ctx, pkg):
|
||||||
ctx.run(["make", "DESTDIR=" + pkg.destdir, "install-gcc"])
|
ctx.run(["make", "install-gcc"], env = {"DESTDIR": pkg.dest_dir})
|
||||||
ctx.run(["make", "DESTDIR=" + pkg.destdir, "install-target-libgcc"])
|
ctx.run(["make", "install-target-libgcc"], env = {"DESTDIR": pkg.dest_dir})
|
||||||
+178
-1
@@ -1 +1,178 @@
|
|||||||
pub struct Builder;
|
use std::{
|
||||||
|
fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
process::Command,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{Context, bail};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
config::Config,
|
||||||
|
graph::{TaskPlan, TaskPlanner},
|
||||||
|
log,
|
||||||
|
recipe::RecipeSet,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[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 {
|
||||||
|
bail!("task execution is not implemented yet");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
bail!("task execution is not implemented yet");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
.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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+18
-4
@@ -46,6 +46,8 @@ 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<()> {
|
||||||
@@ -53,11 +55,23 @@ pub fn run() -> anyhow::Result<()> {
|
|||||||
|
|
||||||
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::load(&root_path.join("config.star"))?;
|
||||||
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
|
|
||||||
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
|
|
||||||
|
|
||||||
match cli.command {
|
match cli.command {
|
||||||
Command::Fetch { .. } => todo!(),
|
Command::Fetch(command) => {
|
||||||
Command::Build { .. } => todo!(),
|
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
|
||||||
|
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
|
||||||
|
let mut builder = Builder::new(root_path, config);
|
||||||
|
builder.fetch(&recipes, &command.recipes, command.dry_run)
|
||||||
|
}
|
||||||
|
Command::Build(command) => {
|
||||||
|
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
|
||||||
|
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
|
||||||
|
let mut builder = Builder::new(root_path, config);
|
||||||
|
builder.build(&recipes, &command.recipes, command.rebuild, command.dry_run)
|
||||||
|
}
|
||||||
|
Command::Image => {
|
||||||
|
let mut builder = Builder::new(root_path, config);
|
||||||
|
builder.ensure_container_ready()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,14 +12,31 @@ use crate::{
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ContainerRuntime {
|
pub enum ContainerRuntime {
|
||||||
|
Docker,
|
||||||
Podman,
|
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 {
|
impl TryFrom<&str> for ContainerRuntime {
|
||||||
type Error = anyhow::Error;
|
type Error = anyhow::Error;
|
||||||
|
|
||||||
fn try_from(value: &str) -> anyhow::Result<Self> {
|
fn try_from(value: &str) -> anyhow::Result<Self> {
|
||||||
match value {
|
match value {
|
||||||
|
"docker" => Ok(Self::Docker),
|
||||||
"podman" => Ok(Self::Podman),
|
"podman" => Ok(Self::Podman),
|
||||||
_ => anyhow::bail!("invalid runtime: {value}"),
|
_ => anyhow::bail!("invalid runtime: {value}"),
|
||||||
}
|
}
|
||||||
|
|||||||
+552
@@ -0,0 +1,552 @@
|
|||||||
|
use std::{
|
||||||
|
collections::{BTreeMap, BTreeSet},
|
||||||
|
fmt, fs,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::{Context, bail};
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
|
use crate::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> {
|
||||||
|
root: &'a Path,
|
||||||
|
arch: &'a str,
|
||||||
|
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 {
|
||||||
|
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 mut order = Vec::new();
|
||||||
|
let mut visiting = BTreeSet::new();
|
||||||
|
let mut visited = BTreeSet::new();
|
||||||
|
for task in self.dependencies.keys() {
|
||||||
|
topo_visit(
|
||||||
|
task,
|
||||||
|
&self.dependencies,
|
||||||
|
&mut visiting,
|
||||||
|
&mut visited,
|
||||||
|
&mut order,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
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())?;
|
||||||
|
self.output_task_active(recipe, output, "install")
|
||||||
|
}
|
||||||
|
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.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.source_stamp(recipe, "version"))
|
||||||
|
.ok()
|
||||||
|
.as_deref()
|
||||||
|
!= Some(want_version.as_str())
|
||||||
|
{
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
if self.recipe_has_patches(recipe)? && !self.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.recipe_task_stamp(recipe, kind))
|
||||||
|
.ok()
|
||||||
|
.as_deref()
|
||||||
|
!= Some(self.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.output_task_stamp(output, kind))
|
||||||
|
.ok()
|
||||||
|
.as_deref()
|
||||||
|
!= Some(self.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.apk_path(recipe, output).exists() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Ok(fs::read_to_string(self.output_task_stamp(output, "apk"))
|
||||||
|
.ok()
|
||||||
|
.as_deref()
|
||||||
|
!= Some(self.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.host_install_dir(recipe).exists() {
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
Ok(
|
||||||
|
fs::read_to_string(self.recipe_task_stamp(recipe, "host-install"))
|
||||||
|
.ok()
|
||||||
|
.as_deref()
|
||||||
|
!= Some(self.recipe_fingerprint(recipe)?.as_str()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recipe_has_patches(&self, recipe: &Recipe) -> anyhow::Result<bool> {
|
||||||
|
Ok(!self.recipe_patches(recipe)?.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_cache_path(&self, key: &str) -> PathBuf {
|
||||||
|
self.root.join("build/cache/sources").join(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn source_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf {
|
||||||
|
self.root
|
||||||
|
.join("build/sources")
|
||||||
|
.join(format!("{}.{kind}", recipe.slug()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn recipe_task_stamp(&self, recipe: &Recipe, kind: &str) -> PathBuf {
|
||||||
|
self.root
|
||||||
|
.join("build/tasks")
|
||||||
|
.join(format!("{}.{kind}", recipe.slug()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output_task_stamp(&self, output: &OutputPackage, kind: &str) -> PathBuf {
|
||||||
|
self.root
|
||||||
|
.join("build/tasks")
|
||||||
|
.join(format!("{}.{kind}", output.key().replace(':', "-")))
|
||||||
|
}
|
||||||
|
|
||||||
|
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()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn host_install_dir(&self, recipe: &Recipe) -> PathBuf {
|
||||||
|
self.root
|
||||||
|
.join("build/host-pkgs")
|
||||||
|
.join(recipe.slug())
|
||||||
|
.join("usr/local")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn topo_visit(
|
||||||
|
task: &TaskId,
|
||||||
|
dependencies: &BTreeMap<TaskId, Vec<TaskId>>,
|
||||||
|
visiting: &mut BTreeSet<TaskId>,
|
||||||
|
visited: &mut BTreeSet<TaskId>,
|
||||||
|
order: &mut Vec<TaskId>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
if visited.contains(task) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if !visiting.insert(task.clone()) {
|
||||||
|
bail!("task dependency cycle involving `{task}`");
|
||||||
|
}
|
||||||
|
for dependency in dependencies.get(task).into_iter().flatten() {
|
||||||
|
topo_visit(dependency, dependencies, visiting, visited, order)?;
|
||||||
|
}
|
||||||
|
visiting.remove(task);
|
||||||
|
visited.insert(task.clone());
|
||||||
|
order.push(task.clone());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
use tempfile::TempDir;
|
||||||
|
|
||||||
|
use crate::{config::Config, eval, recipe::RecipeSet};
|
||||||
|
|
||||||
|
use super::{TaskId, TaskPlanner};
|
||||||
|
|
||||||
|
fn write_config(root: &TempDir) {
|
||||||
|
fs::write(
|
||||||
|
root.path().join("config.star"),
|
||||||
|
r#"
|
||||||
|
container_runtime = "podman"
|
||||||
|
container_image = "local/test:latest"
|
||||||
|
container_dockerfile = "Dockerfile"
|
||||||
|
arch = "x86_64"
|
||||||
|
options = dict(target_arch = arch)
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
fs::write(root.path().join("Dockerfile"), "FROM scratch\n").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_recipe(root: &TempDir, dir: &str, name: &str, extra: &str) {
|
||||||
|
let recipe_dir = root.path().join(dir);
|
||||||
|
fs::create_dir_all(&recipe_dir).unwrap();
|
||||||
|
fs::write(
|
||||||
|
recipe_dir.join(format!("{name}.star")),
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
version = "1.0"
|
||||||
|
revision = 1
|
||||||
|
source = tarball_source(url = "file:///tmp/{name}.tar", sha256 = "hash-{name}")
|
||||||
|
{extra}
|
||||||
|
def build(ctx):
|
||||||
|
ctx.run(["true"])
|
||||||
|
def install(ctx, pkg):
|
||||||
|
ctx.run(["true"])
|
||||||
|
"#
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(root: &TempDir) -> (Config, RecipeSet) {
|
||||||
|
let config = Config::load(&root.path().join("config.star")).unwrap();
|
||||||
|
let lib = eval::eval_lib(&root.path().join("lib"), Some(&config.options)).unwrap();
|
||||||
|
let recipes = RecipeSet::load(root.path(), &config.options, lib.as_ref()).unwrap();
|
||||||
|
(config, recipes)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn inactive_seed_does_not_pull_dependencies() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
write_config(&root);
|
||||||
|
write_recipe(&root, "recipes", "dep", "");
|
||||||
|
write_recipe(&root, "recipes", "app", r#"deps = ["dep"]"#);
|
||||||
|
let (config, recipes) = load(&root);
|
||||||
|
|
||||||
|
let planner = TaskPlanner::new(root.path(), &config.arch, &recipes);
|
||||||
|
let output = recipes.output("app").unwrap();
|
||||||
|
let recipe = recipes.recipe(output.recipe()).unwrap();
|
||||||
|
fs::create_dir_all(root.path().join("build/pkgs/x86_64")).unwrap();
|
||||||
|
fs::write(root.path().join("build/pkgs/x86_64/app-1.0-r1.apk"), "").unwrap();
|
||||||
|
fs::create_dir_all(root.path().join("build/tasks")).unwrap();
|
||||||
|
fs::write(
|
||||||
|
root.path().join("build/tasks/app.apk"),
|
||||||
|
planner.output_fingerprint(recipe, output).unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let plan = TaskPlanner::new(root.path(), &config.arch, &recipes)
|
||||||
|
.build_plan(&["app".to_owned()], false)
|
||||||
|
.unwrap();
|
||||||
|
assert!(plan.is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn active_target_recipe_keeps_edges_and_topo_order() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
write_config(&root);
|
||||||
|
write_recipe(&root, "recipes", "app", "");
|
||||||
|
let (config, recipes) = load(&root);
|
||||||
|
|
||||||
|
let plan = TaskPlanner::new(root.path(), &config.arch, &recipes)
|
||||||
|
.build_plan(&["app".to_owned()], false)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
plan.order(),
|
||||||
|
&[
|
||||||
|
TaskId::FetchSources("app".to_owned()),
|
||||||
|
TaskId::PrepareSources("app".to_owned()),
|
||||||
|
TaskId::ConfigureRecipe("app".to_owned()),
|
||||||
|
TaskId::BuildRecipe("app".to_owned()),
|
||||||
|
TaskId::InstallPackageFiles("app".to_owned()),
|
||||||
|
TaskId::ProduceApk("app".to_owned()),
|
||||||
|
]
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
plan.dependencies(&TaskId::ProduceApk("app".to_owned())),
|
||||||
|
Some([TaskId::InstallPackageFiles("app".to_owned())].as_slice())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn host_dependencies_are_installed_as_host_recipes() {
|
||||||
|
let root = TempDir::new().unwrap();
|
||||||
|
write_config(&root);
|
||||||
|
write_recipe(&root, "host-recipes", "binutils", "");
|
||||||
|
write_recipe(&root, "recipes", "app", r#"host_deps = ["binutils"]"#);
|
||||||
|
let (config, recipes) = load(&root);
|
||||||
|
|
||||||
|
let plan = TaskPlanner::new(root.path(), &config.arch, &recipes)
|
||||||
|
.build_plan(&["app".to_owned()], false)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
plan.order()
|
||||||
|
.contains(&TaskId::InstallHostRecipe("host:binutils".to_owned()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
use std::{io::IsTerminal, sync::LazyLock};
|
||||||
|
|
||||||
|
static IS_STDERR_TERMINAL: LazyLock<bool> = LazyLock::new(|| std::io::stderr().is_terminal());
|
||||||
|
|
||||||
|
const ARROW: &str = "==>";
|
||||||
|
|
||||||
|
fn emit(color: &str, action: &str, details: &str) {
|
||||||
|
if *IS_STDERR_TERMINAL {
|
||||||
|
eprintln!("\x1b[{color}m{ARROW} \x1b[1m{action} \x1b[0m{details}");
|
||||||
|
} else {
|
||||||
|
eprintln!("{ARROW} {action} {details}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn step(action: &str, details: &str) {
|
||||||
|
emit("1;34", action, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn skip(action: &str, details: &str) {
|
||||||
|
emit("1;33", action, details);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(dead_code)]
|
||||||
|
pub fn info(action: &str, details: &str) {
|
||||||
|
emit("1;32", action, details);
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ mod builder;
|
|||||||
mod cli;
|
mod cli;
|
||||||
mod config;
|
mod config;
|
||||||
mod eval;
|
mod eval;
|
||||||
|
mod graph;
|
||||||
|
mod log;
|
||||||
mod options;
|
mod options;
|
||||||
mod recipe;
|
mod recipe;
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ impl RecipeKind {
|
|||||||
Self::HostPackage => format!("host:{name}"),
|
Self::HostPackage => format!("host:{name}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn slug(self, name: &str) -> String {
|
||||||
|
self.key(name).replace(':', "-")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -44,7 +48,25 @@ pub enum Sources {
|
|||||||
Multiple(HashMap<String, 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 {
|
pub struct Recipe {
|
||||||
|
/// Recipe name without namespace prefix.
|
||||||
|
name: String,
|
||||||
/// Path to the recipe's .star file.
|
/// Path to the recipe's .star file.
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
/// What kind of a recipe is that?
|
/// What kind of a recipe is that?
|
||||||
@@ -173,6 +195,7 @@ impl Recipe {
|
|||||||
let phases = RecipePhases::load(&module)?;
|
let phases = RecipePhases::load(&module)?;
|
||||||
|
|
||||||
Ok(Recipe {
|
Ok(Recipe {
|
||||||
|
name: name.to_owned(),
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
kind,
|
kind,
|
||||||
version,
|
version,
|
||||||
@@ -190,6 +213,58 @@ impl Recipe {
|
|||||||
pub fn phases(&self) -> &RecipePhases {
|
pub fn phases(&self) -> &RecipePhases {
|
||||||
&self.phases
|
&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>> {
|
fn optional_string_list(module: &Module, key: &str) -> anyhow::Result<Vec<String>> {
|
||||||
@@ -281,6 +356,20 @@ pub struct OutputPackage {
|
|||||||
metadata: Metadata,
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct RecipeSet {
|
pub struct RecipeSet {
|
||||||
recipes: HashMap<String, Recipe>,
|
recipes: HashMap<String, Recipe>,
|
||||||
@@ -329,6 +418,18 @@ impl RecipeSet {
|
|||||||
|
|
||||||
Ok(Self { recipes, outputs })
|
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
|
/// Find all recipe `.star` files under `dir`, returning a map of recipe name
|
||||||
|
|||||||
@@ -89,3 +89,23 @@ starlark::starlark_simple_value!(Source);
|
|||||||
|
|
||||||
#[starlark_value(type = "source")]
|
#[starlark_value(type = "source")]
|
||||||
impl<'v> StarlarkValue<'v> for 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(), "?" | "???")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user