Skip to content

Commit e59695f

Browse files
committed
Remove use of v0::Opt from binary builder
This changes the current inheritance mechanism from being calculated based on the specific v0::Opt::inheritance field, instead expecting the package itself to identify downstream requests that need to be present. In short, this means that users need to add explicit requests to satisfy the upstream packages. As much as this is more work for users, it ensures that the final nature of each one remains up to the package definition so that they can be based on when conditions and/or attached only to specific components which is not possible now. Signed-off-by: Ryan Bottriell <rbottriell@ilm.com>
1 parent f4cbcd8 commit e59695f

File tree

14 files changed

+492
-157
lines changed

14 files changed

+492
-157
lines changed

crates/spk-build/src/build/binary.rs

Lines changed: 61 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,15 @@ use spk_schema::foundation::name::OptNameBuf;
1919
use spk_schema::foundation::option_map::OptionMap;
2020
use spk_schema::foundation::version::VERSION_SEP;
2121
use spk_schema::ident::{PkgRequest, PreReleasePolicy, RangeIdent, RequestedBy, VersionIdent};
22-
use spk_schema::{BuildIdent, ComponentFileMatchMode, ComponentSpecList, Package, PackageMut};
22+
use spk_schema::version::Compatibility;
23+
use spk_schema::{
24+
BuildIdent,
25+
ComponentFileMatchMode,
26+
ComponentSpecList,
27+
Package,
28+
PackageMut,
29+
RequirementsList,
30+
};
2331
use spk_solve::graph::Graph;
2432
use spk_solve::solution::Solution;
2533
use spk_solve::{BoxedResolverCallback, ResolverCallback, Solver};
@@ -264,7 +272,7 @@ where
264272
};
265273

266274
tracing::debug!("Resolving build environment");
267-
let solution = self.resolve_build_environment(&all_options).await?;
275+
let (build_requirements, solution) = self.resolve_build_environment(&all_options).await?;
268276
self.environment
269277
.extend(solution.to_environment(Some(std::env::vars())));
270278

@@ -285,6 +293,7 @@ where
285293
spfs::remount_runtime(&runtime).await?;
286294

287295
let package = self.recipe.generate_binary_build(&all_options, &solution)?;
296+
self.validate_generated_package(&build_requirements, &solution, &package)?;
288297
let components = self
289298
.build_and_commit_artifacts(&package, &all_options)
290299
.await?;
@@ -350,22 +359,68 @@ where
350359
Ok(solution?)
351360
}
352361

353-
async fn resolve_build_environment(&mut self, options: &OptionMap) -> Result<Solution> {
362+
async fn resolve_build_environment(
363+
&mut self,
364+
options: &OptionMap,
365+
) -> Result<(RequirementsList, Solution)> {
354366
self.solver.reset();
355367
self.solver.update_options(options.clone());
356368
self.solver.set_binary_only(true);
357369
for repo in self.repos.iter().cloned() {
358370
self.solver.add_repository(repo);
359371
}
360372

361-
for request in self.recipe.get_build_requirements(options)? {
362-
self.solver.add_request(request);
373+
let build_requirements = self.recipe.get_build_requirements(options)?.into_owned();
374+
for request in build_requirements.iter() {
375+
self.solver.add_request(request.clone());
363376
}
364377

365378
let mut runtime = self.solver.run();
366379
let solution = self.build_resolver.solve(&mut runtime).await;
367380
self.last_solve_graph = runtime.graph();
368-
Ok(solution?)
381+
Ok((build_requirements, solution?))
382+
}
383+
384+
fn validate_generated_package(
385+
&self,
386+
build_requirements: &RequirementsList,
387+
solution: &Solution,
388+
package: &Recipe::Output,
389+
) -> Result<()> {
390+
let runtime_requirements = package.runtime_requirements();
391+
let solved_packages = solution.items().into_iter().map(|r| r.spec);
392+
let all_components = package.components();
393+
for spec in solved_packages {
394+
for component in all_components.names() {
395+
let downstream_build = spec.downstream_build_requirements([component]);
396+
for request in downstream_build.iter() {
397+
match build_requirements.contains_request(request) {
398+
Compatibility::Compatible => continue,
399+
Compatibility::Incompatible(problem) => {
400+
return Err(Error::MissingDownstreamBuildRequest {
401+
required_by: spec.ident().to_owned(),
402+
request: request.clone(),
403+
problem,
404+
})
405+
}
406+
}
407+
}
408+
let downstream_runtime = spec.downstream_build_requirements([component]);
409+
for request in downstream_runtime.iter() {
410+
match runtime_requirements.contains_request(request) {
411+
Compatibility::Compatible => continue,
412+
Compatibility::Incompatible(problem) => {
413+
return Err(Error::MissingDownstreamRuntimeRequest {
414+
required_by: spec.ident().to_owned(),
415+
request: request.clone(),
416+
problem,
417+
})
418+
}
419+
}
420+
}
421+
}
422+
}
423+
Ok(())
369424
}
370425

371426
async fn build_and_commit_artifacts(

crates/spk-build/src/build/binary_test.rs

Lines changed: 78 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,14 @@ use spk_schema::foundation::fixtures::*;
1010
use spk_schema::foundation::ident_component::Component;
1111
use spk_schema::foundation::{opt_name, option_map};
1212
use spk_schema::ident::{PkgRequest, RangeIdent, Request};
13-
use spk_schema::{
14-
recipe,
15-
ComponentSpecList,
16-
FromYaml,
17-
Inheritance,
18-
Opt,
19-
Package,
20-
Recipe,
21-
SpecRecipe,
22-
};
13+
use spk_schema::{recipe, ComponentSpecList, FromYaml, Package, Recipe, SpecRecipe};
2314
use spk_solve::Solution;
2415
use spk_storage::fixtures::*;
2516
use spk_storage::{self as storage, Repository};
2617

2718
use super::{BinaryPackageBuilder, BuildSource};
2819
use crate::build::SourcePackageBuilder;
20+
use crate::Error;
2921

3022
#[rstest]
3123
fn test_split_manifest_permissions() {
@@ -232,7 +224,7 @@ async fn test_build_package_pinning() {
232224
.unwrap();
233225

234226
let spec = rt.tmprepo.read_package(spec.ident()).await.unwrap();
235-
let req = spec.runtime_requirements().get(0).unwrap();
227+
let req = spec.runtime_requirements().first().unwrap().clone();
236228
match req {
237229
Request::Pkg(req) => {
238230
assert_eq!(&req.pkg.to_string(), "dep/~1.0");
@@ -314,12 +306,12 @@ async fn test_build_var_pinning() {
314306
.unwrap();
315307

316308
let spec = rt.tmprepo.read_package(spec.ident()).await.unwrap();
317-
let top_req = spec.runtime_requirements().get(0).unwrap();
309+
let top_req = spec.runtime_requirements().get(0).unwrap().clone();
318310
match top_req {
319311
Request::Var(r) => assert_eq!(&r.value, "topvalue"),
320312
_ => panic!("expected var request"),
321313
}
322-
let depreq = spec.runtime_requirements().get(1).unwrap();
314+
let depreq = spec.runtime_requirements().get(1).unwrap().clone();
323315
match depreq {
324316
Request::Var(r) => assert_eq!(&r.value, "depvalue"),
325317
_ => panic!("expected var request"),
@@ -509,14 +501,14 @@ async fn test_build_filters_unmodified_files() {
509501

510502
#[rstest]
511503
#[tokio::test]
512-
async fn test_build_package_requirement_propagation() {
504+
async fn test_build_package_downstream_build_requests() {
513505
let rt = spfs_runtime().await;
514506
let base_spec = recipe!(
515507
{
516508
"pkg": "base/1.0.0",
517509
"sources": [],
518510
"build": {
519-
"options": [{"var": "inherited/val", "inheritance": "Strong"}],
511+
"options": [{"var": "inherited/val", "inheritance": "StrongForBuildOnly"}],
520512
"script": "echo building...",
521513
},
522514
}
@@ -528,14 +520,14 @@ async fn test_build_package_requirement_propagation() {
528520
"build": {"options": [{"pkg": "base"}], "script": "echo building..."},
529521
}
530522
);
531-
rt.tmprepo.publish_recipe(&base_spec).await.unwrap();
532523
rt.tmprepo.publish_recipe(&top_spec).await.unwrap();
524+
rt.tmprepo.publish_recipe(&base_spec).await.unwrap();
533525

534526
SourcePackageBuilder::from_recipe(base_spec.clone())
535527
.build_and_publish(".", &*rt.tmprepo)
536528
.await
537529
.unwrap();
538-
let _base_pkg = BinaryPackageBuilder::from_recipe(base_spec)
530+
let (base_pkg, _) = BinaryPackageBuilder::from_recipe(base_spec)
539531
.with_repository(rt.tmprepo.clone())
540532
.build_and_publish(&*rt.tmprepo)
541533
.await
@@ -545,45 +537,85 @@ async fn test_build_package_requirement_propagation() {
545537
.build_and_publish(".", &*rt.tmprepo)
546538
.await
547539
.unwrap();
548-
let (top_pkg, _) = BinaryPackageBuilder::from_recipe(top_spec)
540+
let result = BinaryPackageBuilder::from_recipe(top_spec)
549541
.with_repository(rt.tmprepo.clone())
550542
.build_and_publish(&*rt.tmprepo)
551-
.await
552-
.unwrap();
543+
.await;
553544

554-
assert_eq!(top_pkg.options().len(), 2, "should get option added");
555-
let opt = top_pkg.options().get(1).unwrap();
556-
match opt {
557-
Opt::Var(opt) => {
558-
assert_eq!(
559-
&*opt.var, "base.inherited",
560-
"should be inherited as package option"
561-
);
562-
assert_eq!(
563-
opt.inheritance,
564-
Inheritance::Weak,
565-
"inherited option should have weak inheritance"
545+
match result {
546+
Err(Error::MissingDownstreamBuildRequest {
547+
required_by,
548+
request,
549+
..
550+
}) => {
551+
assert_eq!(&required_by, base_pkg.ident());
552+
assert!(
553+
matches!(&request, Request::Var(v) if v.var.as_str() == "base.inherited" && v.value == "val"),
554+
"{request}"
566555
);
567556
}
568-
_ => panic!("should be given inherited option"),
557+
Err(err) => panic!("Expected Error::MissingDownstreamBuildRequest, got {err:?}"),
558+
Ok(_) => panic!("should error when downstream package does not define inherited opt"),
569559
}
560+
}
570561

571-
assert_eq!(
572-
top_pkg.runtime_requirements().len(),
573-
1,
574-
"should get install requirement"
562+
#[rstest]
563+
#[tokio::test]
564+
async fn test_build_package_downstream_runtime_request() {
565+
let rt = spfs_runtime().await;
566+
let base_spec = recipe!(
567+
{
568+
"pkg": "base/1.0.0",
569+
"sources": [],
570+
"build": {
571+
"options": [{"var": "inherited/val", "inheritance": "Strong"}],
572+
"script": "echo building...",
573+
},
574+
}
575575
);
576-
let req = top_pkg.runtime_requirements().get(0).unwrap();
577-
match req {
578-
Request::Var(req) => {
579-
assert_eq!(
580-
&*req.var, "base.inherited",
581-
"should be inherited with package namespace"
576+
let top_spec = recipe!(
577+
{
578+
"pkg": "top/1.0.0",
579+
"sources": [],
580+
"build": {"options": [{"pkg": "base"}, {"var": "inherited/val"}], "script": "echo building..."},
581+
}
582+
);
583+
rt.tmprepo.publish_recipe(&top_spec).await.unwrap();
584+
rt.tmprepo.publish_recipe(&base_spec).await.unwrap();
585+
586+
SourcePackageBuilder::from_recipe(base_spec.clone())
587+
.build_and_publish(".", &*rt.tmprepo)
588+
.await
589+
.unwrap();
590+
let (base_pkg, _) = BinaryPackageBuilder::from_recipe(base_spec)
591+
.with_repository(rt.tmprepo.clone())
592+
.build_and_publish(&*rt.tmprepo)
593+
.await
594+
.unwrap();
595+
596+
SourcePackageBuilder::from_recipe(top_spec.clone())
597+
.build_and_publish(".", &*rt.tmprepo)
598+
.await
599+
.unwrap();
600+
let result = BinaryPackageBuilder::from_recipe(top_spec)
601+
.with_repository(rt.tmprepo.clone())
602+
.build_and_publish(&*rt.tmprepo)
603+
.await;
604+
605+
match result {
606+
Err(Error::MissingDownstreamRuntimeRequest {
607+
required_by,
608+
request,
609+
..
610+
}) => {
611+
assert_eq!(&required_by, base_pkg.ident());
612+
assert!(
613+
matches!(&request, Request::Var(v) if v.var.as_str() == "base.inherited" && v.value == "val"),
614+
"{request}"
582615
);
583-
assert!(!req.pin, "should not be pinned after build");
584-
assert_eq!(req.value, "val", "should be rendered to build time var");
585616
}
586-
_ => panic!("should be given var request"),
617+
Err(err) => panic!("Expected Error::MissingDownstreamRuntimeRequest, got {err}"),
618+
Ok(_) => panic!("should error when downstream package does not define inherited opt"),
587619
}
588620
}
589621

crates/spk-build/src/error.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// SPDX-License-Identifier: Apache-2.0
33
// https://github.com/imageworks/spk
44

5+
use spk_schema::BuildIdent;
6+
use spk_solve::Request;
57
use thiserror::Error;
68

79
pub type Result<T> = std::result::Result<T, Error>;
@@ -20,6 +22,24 @@ pub enum Error {
2022
FileWriteError(std::path::PathBuf, #[source] std::io::Error),
2123
#[error(transparent)]
2224
ProcessSpawnError(spfs::Error),
25+
#[error("Package must include a build requirement for {request}, because it's being built against {required_by}, but {problem}")]
26+
MissingDownstreamBuildRequest {
27+
/// The package that was in the build environment and created the need for this request
28+
required_by: BuildIdent,
29+
/// The minimum request that is required downstream
30+
request: Request,
31+
/// Additional reasoning why an existing request was not sufficient
32+
problem: String,
33+
},
34+
#[error("Package must include a runtime requirement for {request}, because it's being built against {required_by}, but {problem}")]
35+
MissingDownstreamRuntimeRequest {
36+
/// The package that was in the build environment and created the need for this request
37+
required_by: BuildIdent,
38+
/// The minimum request that is required downstream
39+
request: Request,
40+
/// Additional reasoning why an existing request was not sufficient
41+
problem: String,
42+
},
2343
#[error(transparent)]
2444
SPFS(#[from] spfs::Error),
2545
#[error(transparent)]

crates/spk-cli/common/src/flags.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,7 @@ impl Requests {
319319

320320
TestStage::Build => {
321321
let requirements = recipe.get_build_requirements(&options)?;
322-
out.extend(requirements);
322+
out.extend(requirements.into_owned());
323323
}
324324
TestStage::Install => out.push(
325325
PkgRequest::from_ident_exact(

crates/spk-schema/crates/foundation/src/version/compat.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,12 @@ impl std::ops::Not for &'_ Compatibility {
113113
}
114114

115115
impl Compatibility {
116+
/// Creates a compatibility instance denoting incompatibility
117+
/// for the provided reason
118+
pub fn incompatible<S: Into<String>>(reason: S) -> Self {
119+
Self::Incompatible(reason.into())
120+
}
121+
116122
pub fn is_ok(&self) -> bool {
117123
matches!(self, &Compatibility::Compatible)
118124
}

0 commit comments

Comments
 (0)