Skip to content

Commit 161ce08

Browse files
authored
Update label_injections to work transitively (bazelbuild#4081)
Rendering canonical labels into lockfiles is sensitive to consumers potentially having overrides. Instead, injections are applied on the fly.
1 parent 5932417 commit 161ce08

16 files changed

Lines changed: 522 additions & 244 deletions

File tree

crate_universe/src/cli/generate.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,21 @@ pub struct GenerateOptions {
113113
}
114114

115115
pub fn generate(opt: GenerateOptions) -> Result<()> {
116-
// Load the config
116+
// Load the config. `config.label_injection_mapping` is the per-session
117+
// apparent -> canonical map reflecting the root module's current
118+
// `single_version_override` / `multiple_version_override` choices. It is
119+
// applied to the Context just before rendering and is sanitized out of
120+
// the digest hash in `Digest::new`, so consumer-side overrides don't
121+
// force a producer-side repin (the producer's lockfile may live in a
122+
// read-only bzlmod cache).
117123
let config = Config::try_from_path(&opt.config)?;
118124

119125
// Go straight to rendering if there is no need to repin
120126
if !opt.repin {
121127
if let Some(lockfile) = &opt.lockfile {
122-
let context = Context::try_from_path(lockfile)?;
128+
// Lockfile stores apparent labels; rewrite to canonical here.
129+
let context = Context::try_from_path(lockfile)?
130+
.apply_label_injection_mapping(&config.label_injection_mapping)?;
123131

124132
// Render build files
125133
let outputs = Renderer::new(
@@ -205,15 +213,23 @@ pub fn generate(opt: GenerateOptions) -> Result<()> {
205213
&opt.nonhermetic_root_bazel_workspace_dir,
206214
)?;
207215

208-
// Generate renderable contexts for each package
216+
// Generate renderable contexts for each package. The Context here holds
217+
// the user's APPARENT labels (e.g. `@openssl//:install`) because the
218+
// label_injection mapping was detached at config load.
209219
let context = Context::new(annotations, config.rendering.are_sources_present())?;
210220

211-
// Render build files
221+
// Render build files. Apply the apparent -> canonical mapping just for
222+
// rendering — the lockfile written below uses the original apparent
223+
// Context, so consumer-side overrides stay sound without producer-side
224+
// repin.
225+
let render_context = context
226+
.clone()
227+
.apply_label_injection_mapping(&config.label_injection_mapping)?;
212228
let outputs = Renderer::new(
213229
Arc::new(config.rendering.clone()),
214230
Arc::new(config.supported_platform_triples.clone()),
215231
)
216-
.render(&context, opt.generator)?;
232+
.render(&render_context, opt.generator)?;
217233

218234
// make file paths compatible with bazel labels
219235
let normalized_outputs = normalize_cargo_file_paths(outputs, &opt.repository_dir);

crate_universe/src/cli/query.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,11 @@ pub fn query(opt: QueryOptions) -> Result<()> {
6161
None => bail!("No digest provided in lockfile"),
6262
};
6363

64-
// Load the config file
64+
// Load the config file. `config.label_injection_mapping` is populated
65+
// but unused for the digest check — `Digest::new` sanitizes it out
66+
// before hashing so this comparison stays stable across consumer-side
67+
// overrides (e.g., `single_version_override` on an injected dep);
68+
// otherwise every override would force a producer-side repin.
6569
let config = Config::try_from_path(&opt.config)?;
6670

6771
let splicing_manifest = SplicingManifest::try_from_path(&opt.splicing_manifest)?;

crate_universe/src/cli/splice.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,8 @@ pub fn splice(opt: SpliceOptions) -> Result<()> {
127127
.context("Failed to generate lockfile")?
128128
};
129129

130+
// Splice doesn't render BUILD files; `config.label_injection_mapping` is
131+
// populated but unused here. The substitution happens in `generate`.
130132
let config = Config::try_from_path(&opt.config).context("Failed to parse config")?;
131133

132134
let resolver_data = TreeResolver::new(cargo.clone())

crate_universe/src/cli/vendor.rs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,9 @@ pub fn vendor(opt: VendorOptions) -> anyhow::Result<()> {
251251
&opt.repin,
252252
)?;
253253

254-
// Load the config from disk
254+
// Load the config from disk. `config.label_injection_mapping` is applied
255+
// to the Context just before render (see near `Renderer::new` below) and
256+
// is sanitized out of the digest hash by `Digest::new`.
255257
let config = Config::try_from_path(&opt.config)?;
256258

257259
let resolver_data = TreeResolver::new(cargo.clone()).generate(
@@ -288,6 +290,13 @@ pub fn vendor(opt: VendorOptions) -> anyhow::Result<()> {
288290
// Generate renderable contexts for search package
289291
let context = Context::new(annotations, config.rendering.are_sources_present())?;
290292

293+
// Apply label_injection just before render. The Context at this point
294+
// contains the user's apparent labels (e.g. `@openssl//:install`); the
295+
// mapping rewrites them to the per-session canonical (e.g.
296+
// `@@openssl+v3.5.5//:install`), reflecting whatever the root module's
297+
// current `single_version_override` etc. resolved to.
298+
let context = context.apply_label_injection_mapping(&config.label_injection_mapping)?;
299+
291300
// Render build files
292301
let outputs = Renderer::new(
293302
Arc::new(config.rendering.clone()),

crate_universe/src/config.rs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
//! A module for configuration information
22
3-
mod label_injection;
3+
pub(crate) mod label_injection;
44

55
use std::cmp::Ordering;
66
use std::collections::{BTreeMap, BTreeSet};
@@ -719,6 +719,18 @@ pub(crate) struct Config {
719719
/// A set of platform triples to use in generated select statements
720720
#[serde(default, skip_serializing_if = "BTreeSet::is_empty")]
721721
pub(crate) supported_platform_triples: BTreeSet<TargetTriple>,
722+
723+
/// Apparent -> canonical label_injection map extracted from each
724+
/// annotation's `label_injections` field at load time. Populated by
725+
/// `Config::try_from_path`; not present in config.json itself
726+
/// (`deny_unknown_fields` is fine because `extract_global_mapping`
727+
/// strips `label_injections` before deserialization). Sanitized to
728+
/// `Default::default()` in `Digest::new` before hashing — same trick
729+
/// as `Context.checksum = None` in `lockfile.rs` — so consumer-side
730+
/// `single_version_override` shifts (which change the canonical names
731+
/// here) don't perturb the digest and force a producer-side repin.
732+
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
733+
pub(crate) label_injection_mapping: LabelInjectionMapping,
722734
}
723735

724736
// rules_rust/crate_universe/private/generate_utils.bzl:generate_config
@@ -744,14 +756,34 @@ where
744756
}
745757

746758
impl Config {
759+
/// Load a config JSON and extract its per-annotation `label_injections`
760+
/// into a single `label_injection_mapping` on the Config. The Config
761+
/// itself keeps the user's APPARENT labels in annotation strings — the
762+
/// rewrite to canonical happens via
763+
/// [`label_injection::apply_mapping_to_value`] just before each render
764+
/// (see [`crate::context::Context::apply_label_injection_mapping`]),
765+
/// using whatever mapping the current bazel session resolved (reflecting
766+
/// any root-level `single_version_override` / `multiple_version_override`).
767+
///
768+
/// The mapping is excluded from the digest hash via
769+
/// [`crate::lockfile::Digest::new`], mirroring the
770+
/// `Context.checksum = None` clear in the same file. That's what keeps
771+
/// the digest stable across consumer-side overrides.
747772
pub(crate) fn try_from_path<T: AsRef<Path>>(path: T) -> Result<Self> {
748773
let data = fs::read_to_string(path)?;
749774
let mut value: serde_json::Value = serde_json::from_str(&data)?;
750-
label_injection::apply(&mut value);
751-
Ok(serde_json::from_value(value)?)
775+
let mapping = label_injection::extract_global_mapping(&mut value)?;
776+
let mut config: Self = serde_json::from_value(value)?;
777+
config.label_injection_mapping = mapping;
778+
Ok(config)
752779
}
753780
}
754781

782+
/// Apparent-repo-prefix -> canonical-repo-prefix map carried on
783+
/// [`Config::label_injection_mapping`]. See [`label_injection`] for the WHY
784+
/// of the deferred-substitution design.
785+
pub(crate) type LabelInjectionMapping = std::collections::BTreeMap<String, String>;
786+
755787
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
756788
pub struct CrateNameAndVersionReq {
757789
/// The name of the crate

0 commit comments

Comments
 (0)