wip 2
This commit is contained in:
@@ -1,23 +1,22 @@
|
||||
name = "gcc"
|
||||
version = "16.1.0"
|
||||
revision = 1
|
||||
description = "GNU GCC cross-compiler (bootstrap stage, C/C++ only)"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
source = {
|
||||
"url": f"https://ftp.gnu.org/gnu/gcc/gcc-{version}/gcc-{version}.tar.xz",
|
||||
"sha256": "50efb4d94c3397aff3b0d61a5abd748b4dd31d9d3f2ab7be05b171d36a510f79",
|
||||
"strip_components": 1,
|
||||
}
|
||||
|
||||
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,
|
||||
"--target=" + OPTIONS.target_triple,
|
||||
"--with-sysroot=" + ctx.prefix + "/" + OPTIONS.target_triple,
|
||||
"--with-sysroot=" + ctx.sysroot,
|
||||
"--without-headers",
|
||||
"--with-newlib",
|
||||
"--enable-languages=c,c++",
|
||||
@@ -33,9 +32,9 @@ def configure(ctx):
|
||||
"--disable-libvtv",
|
||||
"--disable-multilib",
|
||||
], env = {
|
||||
"CFLAGS": OPTIONS.host_cflags,
|
||||
"CXXFLAGS": OPTIONS.host_cxxflags,
|
||||
"LDFLAGS": OPTIONS.host_ldflags,
|
||||
"CFLAGS": options.host_cflags,
|
||||
"CXXFLAGS": options.host_cxxflags,
|
||||
"LDFLAGS": options.host_ldflags,
|
||||
})
|
||||
|
||||
def build(ctx):
|
||||
@@ -44,5 +43,5 @@ def build(ctx):
|
||||
ctx.run(["make", jobs, "all-target-libgcc"])
|
||||
|
||||
def install(ctx, pkg):
|
||||
ctx.run(["make", "DESTDIR=" + pkg.destdir, "install-gcc"])
|
||||
ctx.run(["make", "DESTDIR=" + pkg.destdir, "install-target-libgcc"])
|
||||
ctx.run(["make", "install-gcc"], env = {"DESTDIR": pkg.dest_dir})
|
||||
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 {
|
||||
Fetch(FetchCommand),
|
||||
Build(BuildCommand),
|
||||
#[command(about = "Create or refresh the configured build container image")]
|
||||
Image,
|
||||
}
|
||||
|
||||
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 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 {
|
||||
Command::Fetch { .. } => todo!(),
|
||||
Command::Build { .. } => todo!(),
|
||||
Command::Fetch(command) => {
|
||||
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
|
||||
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
|
||||
let mut builder = Builder::new(root_path, config);
|
||||
builder.fetch(&recipes, &command.recipes, command.dry_run)
|
||||
}
|
||||
Command::Build(command) => {
|
||||
let lib = eval::eval_lib(&root_path.join("lib"), Some(&config.options))?;
|
||||
let recipes = RecipeSet::load(&root_path, &config.options, lib.as_ref())?;
|
||||
let mut builder = Builder::new(root_path, config);
|
||||
builder.build(&recipes, &command.recipes, command.rebuild, command.dry_run)
|
||||
}
|
||||
Command::Image => {
|
||||
let mut builder = Builder::new(root_path, config);
|
||||
builder.ensure_container_ready()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,14 +12,31 @@ use crate::{
|
||||
|
||||
#[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}"),
|
||||
}
|
||||
|
||||
+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 config;
|
||||
mod eval;
|
||||
mod graph;
|
||||
mod log;
|
||||
mod options;
|
||||
mod recipe;
|
||||
|
||||
|
||||
@@ -36,6 +36,10 @@ impl RecipeKind {
|
||||
Self::HostPackage => format!("host:{name}"),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slug(self, name: &str) -> String {
|
||||
self.key(name).replace(':', "-")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -44,7 +48,25 @@ pub enum Sources {
|
||||
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?
|
||||
@@ -173,6 +195,7 @@ impl Recipe {
|
||||
let phases = RecipePhases::load(&module)?;
|
||||
|
||||
Ok(Recipe {
|
||||
name: name.to_owned(),
|
||||
path: path.to_path_buf(),
|
||||
kind,
|
||||
version,
|
||||
@@ -190,6 +213,58 @@ impl Recipe {
|
||||
pub fn phases(&self) -> &RecipePhases {
|
||||
&self.phases
|
||||
}
|
||||
|
||||
pub fn key(&self) -> String {
|
||||
self.kind.key(&self.name)
|
||||
}
|
||||
|
||||
pub fn slug(&self) -> String {
|
||||
self.kind.slug(&self.name)
|
||||
}
|
||||
|
||||
pub fn 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>> {
|
||||
@@ -281,6 +356,20 @@ pub struct OutputPackage {
|
||||
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)]
|
||||
pub struct RecipeSet {
|
||||
recipes: HashMap<String, Recipe>,
|
||||
@@ -329,6 +418,18 @@ impl RecipeSet {
|
||||
|
||||
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
|
||||
|
||||
@@ -89,3 +89,23 @@ 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(), "?" | "???")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user