Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions src/build/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
use anyhow::{Context, Result};
use std::fs;
use std::io::Write;
use std::path::Path;

use super::BuildManifest;
use crate::repo::{get_package, read_manifest};

/// Get the `build_hash` of a `build_manifest`
/// Requires all dependencies to be built and in the Repository beforehand.
///
/// # Errors
///
/// - Scripts do not exist
/// - Invalid build manifest
pub fn calc_build_hash(build_manifest_path: &Path, repo_path: &Path) -> Result<String> {
let build_manifest_path = build_manifest_path.canonicalize().with_context(
|| "could not canoncicalize build manifest path. Does the build manifest exist?",
)?;
let build_manifest_raw = fs::read_to_string(build_manifest_path)?;
let build_manifest: BuildManifest = serde_yaml::from_str(&build_manifest_raw)?;

let repo_manifest = read_manifest(repo_path)?;

let mut hash = blake3::Hasher::new();

hash.write_all(build_manifest_raw.as_bytes())?;

// Hash the `includes`
if let Some(deps) = build_manifest.include {
for dep in deps {
let package = get_package(&repo_manifest, &dep)?;
hash.write_all(package.build_hash.as_bytes())?;
}
}

// Hash the `sdks`
if let Some(deps) = build_manifest.sdks {
for dep in deps {
let package = get_package(&repo_manifest, &dep)?;
hash.write_all(package.build_hash.as_bytes())?;
}
}

// Hash the `build_script`
if let Some(build_script) = build_manifest.build_script {
let script = fs::read_to_string(build_script)?;
hash.write_all(script.as_bytes())?;
}

// Hash the `post_script`
if let Some(post_script) = build_manifest.post_script {
let script = fs::read_to_string(post_script)?;
hash.write_all(script.as_bytes())?;
}

Ok(hash.finalize().to_string())
}

#[cfg(test)]
mod tests {
use std::path::PathBuf;
use temp_dir::TempDir;

use super::*;
use crate::repo::{Metadata, create_repo};

#[test]
fn test_build_hash_stability() {
let manifest = BuildManifest {
id: "test_package".into(),
aliases: Vec::new(),
metadata: Metadata {
description: None,
homepage_url: None,
title: None,
version: None,
license: None,
},
commands: Vec::new(),
directory: PathBuf::from("."),
edition: "2025".into(),
build_script: None,
post_script: None,
sources: None,
include: None,
sdks: None,
env: None,
};

let repo = TempDir::new().unwrap();
create_repo(repo.path(), None).unwrap();

let manifest_path = repo.path().join("build_manifest.yml");

fs::write(&manifest_path, serde_yaml::to_string(&manifest).unwrap()).unwrap();

let known_hash = "680cec2b6b847e76d733fb435214b18ec2108e25b4dfc54695f5daa1e987ec8d";
let calc_hash = calc_build_hash(&manifest_path, repo.path()).unwrap();

assert_eq!(known_hash, calc_hash);
}
}
151 changes: 101 additions & 50 deletions src/build/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod bundle;
pub mod hash;
mod sources;

use anyhow::{Context, Result, bail};
Expand All @@ -11,10 +12,11 @@ use std::{
use temp_dir::TempDir;

use crate::{
build::sources::get_sources,
chunks::{load_tree, save_tree},
repo::{self, Metadata, PackageManifest, get_package, insert_package, read_manifest},
};
use hash::calc_build_hash;
use sources::get_sources;

#[derive(serde::Deserialize, serde::Serialize, Clone)]
struct BuildManifest {
Expand Down Expand Up @@ -70,6 +72,38 @@ pub async fn build(
repo_path: &Path,
config_path: Option<&Path>,
chunk_store_path: &Path,
) -> Result<PackageManifest> {
let repo = read_manifest(repo_path)?;
let build_manifest: BuildManifest =
serde_yaml::from_str(&fs::read_to_string(build_manifest_path)?)?;

if let Ok(package) = get_package(&repo, &build_manifest.id) {
let next_build_hash = calc_build_hash(build_manifest_path, repo_path)?;
if package.build_hash == next_build_hash {
return Ok(package);
}
}

force_build(
build_manifest_path,
repo_path,
config_path,
chunk_store_path,
)
.await
}

/// Builds and inserts a package into a Repository from a `build_manifest`
///
/// # Errors
///
/// - Filesystem (Out of Space, Permissions)
/// - Build Script Failure
pub async fn force_build(
build_manifest_path: &Path,
repo_path: &Path,
config_path: Option<&Path>,
chunk_store_path: &Path,
) -> Result<PackageManifest> {
let build_dir = TempDir::new()?;
let build_manifest_path = &build_manifest_path.canonicalize()?;
Expand All @@ -80,79 +114,53 @@ pub async fn build(
let repo_manifest =
repo::read_manifest(repo_path).with_context(|| "The target Repostiory does not exist")?;

let build_manifest_parent = &build_manifest_path
let search_path = &build_manifest_path
.parent()
.unwrap_or_else(|| Path::new("/"));

if let Some(sources) = build_manifest.sources {
get_sources(build_dir.path(), build_manifest_parent, &sources).await?;
get_sources(build_dir.path(), search_path, &sources).await?;
}

let mut envs = build_manifest.env.unwrap_or_default();

if let Some(packages) = &build_manifest.include {
for dependency in packages {
let result = include(
build_manifest_parent,
dependency,
build_dir.path(),
repo_path,
chunk_store_path,
)?;

envs.extend(result);
}
include_all(
packages,
search_path,
build_dir.path(),
repo_path,
chunk_store_path,
&mut envs,
)?;
}

if let Some(packages) = &build_manifest.sdks {
for dependency in packages {
let result = include(
build_manifest_parent,
dependency,
build_dir.path(),
repo_path,
chunk_store_path,
)?;

envs.extend(result);
}
include_all(
packages,
search_path,
build_dir.path(),
repo_path,
chunk_store_path,
&mut envs,
)?;
}

if let Some(script) = build_manifest.build_script {
let script_path = build_manifest_parent.join(script);

let result = Command::new("sh")
.arg("-c")
.arg(script_path)
.current_dir(build_dir.path())
.status()?;

if !result.success() {
bail!("Build script failed.")
}
run_script(build_dir.path(), search_path, &script).with_context(|| "build_script")?;
}

let out_dir = build_dir.path().join(&build_manifest.directory);

if let Some(script) = build_manifest.post_script {
let script_path = build_manifest_parent.join(script);

let result = Command::new("sh")
.arg("-c")
.arg(script_path)
.current_dir(&out_dir)
.status()?;

if !result.success() {
bail!("Build script failed.")
}
run_script(&out_dir, search_path, &script).with_context(|| "post_script")?;
}

let mut included_chunks = Vec::new();
if let Some(packages) = &build_manifest.include {
for dependency in packages {
include(
build_manifest_parent,
search_path,
dependency,
&out_dir,
repo_path,
Expand All @@ -172,6 +180,7 @@ pub async fn build(
metadata: build_manifest.metadata,
chunks: included_chunks,
env: None,
build_hash: calc_build_hash(build_manifest_path, repo_path)?,
};

if !envs.is_empty() {
Expand All @@ -183,14 +192,39 @@ pub async fn build(
Ok(package_manifest)
}

fn include_all(
packages: &Vec<String>,
search_path: &Path,
build_dir: &Path,
repo_path: &Path,
chunk_store_path: &Path,
envs: &mut HashMap<String, String>,
) -> Result<()> {
for dependency in packages {
let result = include(
search_path,
dependency,
build_dir,
repo_path,
chunk_store_path,
)?;

envs.extend(result);
}

Ok(())
}

/// This requires the dependency to be build first
// Perhaps a future improvement would be to recursively build if not already built? (TODO)
fn include(
build_manifest_parent: &Path,
search_path: &Path,
dependency: &str,
path_to_include_at: &Path,
repo_path: &Path,
chunk_store_path: &Path,
) -> Result<HashMap<String, String>> {
let dependency_build_manifest_path = build_manifest_parent.join(dependency);
let dependency_build_manifest_path = search_path.join(dependency);
let dependency_build_manifest: BuildManifest =
serde_yaml::from_str(&fs::read_to_string(dependency_build_manifest_path)?)?;
let repo_manifest = read_manifest(repo_path)?;
Expand All @@ -204,3 +238,20 @@ fn include(

Ok(dependency_manifest.env.unwrap_or_default())
}

/// Runs a script (typically `post_script` or `build_script`)
fn run_script(cwd: &Path, search_path: &Path, script: &Path) -> Result<()> {
let script_path = search_path.join(script);

let result = Command::new("sh")
.arg("-c")
.arg(script_path)
.current_dir(cwd)
.status()?;

if !result.success() {
bail!("Build script failed.")
}

Ok(())
}
9 changes: 7 additions & 2 deletions src/commands/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::{
};

use flintpkg::{
build::build,
build::{build, force_build},
chunks::{utils::clean_unused, verify_all_chunks},
repo::{
PackageManifest, get_package, read_manifest,
Expand All @@ -21,10 +21,15 @@ pub async fn build_cmd(
repo_name: &str,
build_manifest_path: &Path,
chunk_store_path: &Path,
force: bool,
) -> Result<()> {
let repo_path = resolve_repo(base_path, repo_name)?;

build(build_manifest_path, &repo_path, None, chunk_store_path).await?;
if force {
force_build(build_manifest_path, &repo_path, None, chunk_store_path).await?;
} else {
build(build_manifest_path, &repo_path, None, chunk_store_path).await?;
}

clean_unused(base_path, chunk_store_path)?;

Expand Down
2 changes: 2 additions & 0 deletions src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ pub async fn main_commands(
Command::Build {
build_manifest_path,
repo_name,
force,
} => {
build_cmd(
base_path,
&repo_name,
&build_manifest_path,
chunk_store_path,
force,
)
.await?;
}
Expand Down
2 changes: 2 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ enum Command {
Build {
build_manifest_path: PathBuf,
repo_name: String,
#[arg(long, short)]
force: bool,
},
/// Install a package
Install {
Expand Down
1 change: 1 addition & 0 deletions src/repo/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ mod tests {
license: None,
},
env: None,
build_hash: "Example Build Hash".to_string(),
};

insert_package(&package_manifest, repo_path, Some(repo_path))?;
Expand Down
Loading