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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1076,6 +1076,28 @@ A few inputs are normalized or extended on the way in:

* A source label that names a **directory** hashes the whole tree (entry names, kinds, file
contents and executable bits, symlink targets), so an edit inside it impacts its consumers.
* Outside `--isolatedDependencyRoot`, ordinary query checks native aliases in repositories named
by `--fineGrainedHashExternalRepos` or `--fineGrainedHashExternalReposFile` when returned inputs
reach them. On Bazel 8 and later, a metadata query identifies missing `actual` targets and a
dependency query adds their dependencies and build files. A literal binding on a reached fine
alias is followed through literal ordinary aliases already returned by the primary query until
the first missing destination.
A configurable ordinary alias already in the primary query contributes missing branches only
through the package inventory below. Each reached native alias hashes its evaluated `actual`
binding, including the destination for each `select()` condition. Acquired library rules also
hash the BUILD and Starlark files that define them.
When that dependency query returns rules from external packages, the same input walk also checks
reached ordinary aliases for missing `actual` targets in those packages. One query inventories
the matching packages; only the named targets and their dependencies returned by that inventory
enter the graph. Newly acquired aliases use the same binding hash, and inputs absent from the
inventory retain the existing repository fallback.
Earlier Bazel versions skip this alias acquisition. Full source hashing with these repository
options reads every returned regular external source file from Bazel's output base, including
files in the primary query. The same read applies when no alias query runs. Partial generation
records these digests for offline reuse. Source overrides use repository inputs for files
outside the named repositories.
A supplemental query failure stops strict generation. With `--keep_going`, generation reports
the failure and uses the primary target graph.
* A **repository rule** (`//external:*`) that pins its download with `sha256` or `integrity` is
hashed without its `url`/`urls`, since a mirror or registry move cannot change what it fetches.
The main-repository files its attributes name -- patches, build files, lock files -- are hashed
Expand Down
469 changes: 466 additions & 3 deletions src/bazel.rs

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions src/cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ pub fn configuration_fingerprint(
}
hasher.update(b"stackSourcePaths=1\0");
hasher.update(b"localArchiveBuildFile=1\0");
if !options.bazel.use_cquery
&& !options.bazel.fine_grained_external_repos.is_empty()
&& options.bazel.isolated_dependency_root.is_none()
{
hasher.update(b"fineAliasInputs=4\0");
}
if let Some(root) = &options.bazel.isolated_dependency_root {
hasher.update(b"isolatedDependencyRoot=");
hasher.update(root.as_bytes());
Expand Down
122 changes: 90 additions & 32 deletions src/hash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,24 @@ fn hash_targets_with_environment(
.collect(),
external_resolver,
};
if !options.bazel.use_cquery
&& options.bazel.isolated_dependency_root.is_none()
&& !options.bazel.fine_grained_external_repos.is_empty()
&& (options.partial_tree.is_some() || options.content_hashes.is_none())
&& options.modified_filepaths.is_empty()
{
source_hasher
.external_resolver
.scoped_sources
.queried_files
.extend(
targets
.iter()
.filter_map(|target| target.source_file.as_option())
.filter(|source| !is_main_repo(&source.name))
.map(|source| source.name.clone()),
);
}
let seed_hash = hash_seed_files(&options.bazel.workspace, &options.seed_filepaths)?;
let has_instantiation_stacks = targets.iter().any(|target| {
target.r#type == target::Discriminator::Rule
Expand Down Expand Up @@ -2182,31 +2200,57 @@ impl SourceHasher<'_> {
let key = PathBuf::from("external").join(repo).join(&relative);
(relative, key)
};
// A canonical source returned by cquery can be read directly, even when the
// repository was not queried as a fine-grained root.
if configured_source
&& canonical
&& !repo.is_empty()
&& self.tree.is_none()
&& self.content_hashes.is_none()
&& self.modified_filepaths.is_empty()
{
let (_, key) = paths();
let path = self.external_resolver.output_base.join(&key);
match fs::metadata(&path) {
Ok(metadata) if metadata.is_file() => {
return Ok(Some((path, key.to_string_lossy().into_owned(), true)));
}
Ok(_) => {}
Err(e)
if matches!(
e.kind(),
std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
) => {}
Err(e) => {
return Err(e).with_context(|| {
format!("failed to inspect source {} ({label})", path.display())
})
// Read physical files for returned dependency targets, including ordinary
// aliases whose destinations are outside the fine repository list. Soft labels
// and source overrides continue to use their normal resolver.
let observed = self
.external_resolver
.scoped_sources
.queried_files
.contains(label);
let read_physical = if self.tree.is_some() {
observed
} else {
self.content_hashes.is_none() && (observed || (configured_source && canonical))
};
if read_physical && !repo.is_empty() && self.modified_filepaths.is_empty() {
let canonical_repo = if canonical {
Some(repo)
} else {
self.external_resolver
.repo_mapping()
.get(repo)
.map(String::as_str)
};
if let Some(canonical_repo) = canonical_repo {
let key = PathBuf::from("external")
.join(canonical_repo)
.join(external_label_path(target));
// A partial snapshot supplies Git blobs through the content map. Keep
// its existing resolver when either spelling names a tracked content key.
let from_git = self.content_hashes.is_some_and(|hashes| {
hashes.contains_key(key.to_string_lossy().as_ref())
|| hashes.contains_key(paths().1.to_string_lossy().as_ref())
});
if !from_git {
let path = self.external_resolver.output_base.join(&key);
match fs::metadata(&path) {
Ok(metadata) if metadata.is_file() => {
return Ok(Some((path, key.to_string_lossy().into_owned(), true)));
}
Ok(_) => {}
Err(e)
if matches!(
e.kind(),
std::io::ErrorKind::NotFound
| std::io::ErrorKind::NotADirectory
) => {}
Err(e) => {
return Err(e).with_context(|| {
format!("failed to inspect source {} ({label})", path.display())
})
}
}
}
}
}
Expand Down Expand Up @@ -2501,7 +2545,7 @@ impl ExternalRepoResolver {

/// Parses `bazel mod dump_repo_mapping` output, one JSON object of apparent ->
/// canonical repository names per line, into a flat map.
fn parse_apparent_repo_mapping(text: &str) -> HashMap<String, String> {
pub(crate) fn parse_apparent_repo_mapping(text: &str) -> HashMap<String, String> {
let mut mapping = HashMap::new();
for line in text.lines() {
let Ok(value) = serde_json::from_str::<serde_json::Value>(line.trim()) else {
Expand Down Expand Up @@ -2794,12 +2838,20 @@ mod tests {
#[cfg(unix)]
fn generation_reuses_discovery_outputs() {
let graph = r#"{"key":"<root>","name":"root","dependencies":[]}"#;
for extension_failure in [false, true] {
for (extension_failure, source_repo) in [
(false, "tool"),
(true, "tool"),
(false, "hidden"),
(true, "hidden"),
] {
let workspace = tempfile::tempdir().unwrap();
let external = workspace.path().join("external/local_config_tool");
let external = workspace
.path()
.join(format!("external/local_config_{source_repo}"));
fs::create_dir_all(&external).unwrap();
fs::write(external.join("input"), "mapped input").unwrap();
let target = source_target("@tool//:input", &[]);
let label = format!("@{source_repo}//:input");
let target = source_target(&label, &[]);
let mut encoded = Vec::new();
target.encode_length_delimited(&mut encoded);
fs::write(workspace.path().join("targets.pb"), encoded).unwrap();
Expand All @@ -2816,7 +2868,7 @@ mod tests {
case "$1 $2" in
"mod dump_repo_mapping")
printf '%s\n' mapping >> "$PWD/mapping-calls"
printf '%s\n' '{{"tool":"local_config_tool"}}'
printf '%s\n' '{{"tool":"local_config_tool","hidden":"local_config_hidden"}}'
;;
"mod graph")
printf '%s\n' graph >> "$PWD/graph-calls"
Expand Down Expand Up @@ -2852,15 +2904,15 @@ esac
assert_eq!(
fs::read_to_string(&calls).unwrap().lines().count(),
1,
"extension_failure={extension_failure}"
"extension_failure={extension_failure}, source_repo={source_repo}"
);
assert_eq!(
fs::read_to_string(&mapping_calls).unwrap().lines().count(),
1,
"the resolver reuses the unfiltered root mapping"
);

let direct = hash_targets(&options, vec![target]).unwrap();
let direct = hash_targets(&options, vec![target.clone()]).unwrap();
assert_eq!(direct.hashes, generated.hashes);
assert_eq!(direct.module_graph_json, generated.module_graph_json);
assert_eq!(
Expand All @@ -2873,6 +2925,12 @@ esac
2,
"direct hashing still reads the root mapping"
);
fs::write(external.join("input"), "changed input").unwrap();
let changed = hash_targets(&options, vec![target]).unwrap();
assert_ne!(
direct.hashes[&label].direct_hash,
changed.hashes[&label].direct_hash
);
}
}

Expand Down
27 changes: 19 additions & 8 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -696,7 +696,8 @@ fn fingerprint_flags(
args: &HashingArgs,
include_type: bool,
types: &[String],
) -> BTreeMap<String, String> {
) -> Result<BTreeMap<String, String>> {
let fine_grained_repos = bazel_options(args, false)?.fine_grained_external_repos;
let mut flags = BTreeMap::from([
(
"bazelStartupOptions".into(),
Expand Down Expand Up @@ -728,11 +729,9 @@ fn fingerprint_flags(
),
(
"fineGrainedHashExternalRepos".into(),
args.fine_grained_hash_external_repos
fine_grained_repos
.iter()
.cloned()
.collect::<BTreeSet<_>>()
.into_iter()
.collect::<Vec<_>>()
.join(","),
),
Expand Down Expand Up @@ -765,10 +764,14 @@ fn fingerprint_flags(
("stackSourcePaths".into(), "1".into()),
("localArchiveBuildFile".into(), "1".into()),
]);
if !args.use_cquery && !fine_grained_repos.is_empty() && args.isolated_dependency_root.is_none()
{
flags.insert("fineAliasInputs".into(), "4".into());
}
if let Some(root) = &args.isolated_dependency_root {
flags.insert("isolatedDependencyRoot".into(), root.clone());
}
flags
Ok(flags)
}

fn write_json_file(file: File, value: &impl Serialize) -> Result<()> {
Expand Down Expand Up @@ -1078,7 +1081,7 @@ fn run_explain(args: &ExplainArgs, verbose: bool) -> Result<()> {
}

fn run_fingerprint(args: &FingerprintArgs) -> Result<()> {
let flags = fingerprint_flags(&args.hashing, args.include_target_type, &args.target_type);
let flags = fingerprint_flags(&args.hashing, args.include_target_type, &args.target_type)?;
let inputs = fingerprint::gather(
&args.hashing.workspace(),
&args.hashing.bazel_path,
Expand Down Expand Up @@ -1636,9 +1639,10 @@ mod tests {
second.label_only_module_extensions.reverse();
second.ignored_rule_hashing_attributes.reverse();

let first_flags = fingerprint_flags(&first, true, &["Rule".into(), "GeneratedFile".into()]);
let first_flags =
fingerprint_flags(&first, true, &["Rule".into(), "GeneratedFile".into()]).unwrap();
let second_flags =
fingerprint_flags(&second, true, &["GeneratedFile".into(), "Rule".into()]);
fingerprint_flags(&second, true, &["GeneratedFile".into(), "Rule".into()]).unwrap();
assert_eq!(first_flags, second_flags);
assert_eq!(first_flags["bazelStartupOptions"], "--a --b");
assert_eq!(first_flags["targetType"], "GeneratedFile,Rule");
Expand All @@ -1648,6 +1652,13 @@ mod tests {
first_flags["labelOnlyModuleExtensions"],
"@a//:e.bzl%a,@b//:e.bzl%b"
);
let fine_file = directory.path().join("fine-repos");
fs::write(&fine_file, "from_file\nabc\n").unwrap();
let mut file_only = hashing_args(directory.path());
file_only.fine_grained_hash_external_repos_file = Some(fine_file);
let file_flags = fingerprint_flags(&file_only, false, &[]).unwrap();
assert_eq!(file_flags["fineGrainedHashExternalRepos"], "abc,from_file");
assert_eq!(file_flags["fineAliasInputs"], "4");
}

#[test]
Expand Down
2 changes: 1 addition & 1 deletion src/partial.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type DigestBytes = [u8; 32];

/// Bumped when the recipe format or the evaluation formula changes, so an entry an older
/// release wrote is regenerated rather than evaluated under a formula it was not recorded for.
pub const RECIPE_VERSION: u32 = 4;
pub const RECIPE_VERSION: u32 = 13;

// ---------------------------------------------------------------------------------------------
// Git tree index
Expand Down
Loading
Loading