Skip to content
Open
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
3 changes: 3 additions & 0 deletions crate_universe/extensions.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,9 @@ def _generate_hub_and_spokes(
workspace_name = cfg.name,
generate_binaries = cfg.generate_binaries,
render_config = render_config,
# The hub repository's `crates.bzl` lives outside the module being
# generated for, so first-party labels need an explicit repository.
cargo_lockfile_label = str(cfg.cargo_lockfile) if cfg.cargo_lockfile else None,
repository_ctx = module_ctx,
),
)
Expand Down
11 changes: 11 additions & 0 deletions crate_universe/private/crates_vendor.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,12 @@ def _write_config_file(ctx):
output_pkg = _get_output_package(ctx),
workspace_name = workspace_name,
render_config = dict(json.decode(ctx.attr.render_config)) if ctx.attr.render_config else None,
# Vendored output is committed into the workspace that owns the Cargo
# workspace, so first-party labels stay repository-relative.
cargo_lockfile_label = "//{}:{}".format(
ctx.attr.cargo_lockfile.package,
ctx.attr.cargo_lockfile.name,
) if ctx.attr.cargo_lockfile else None,
),
)

Expand All @@ -285,6 +291,7 @@ def generate_config_file(
output_pkg,
workspace_name,
render_config,
cargo_lockfile_label = None,
repository_ctx = None):
"""Writes the rendering config to cargo-bazel-config.json.

Expand All @@ -301,6 +308,9 @@ def generate_config_file(
output_pkg: The path to the package containing the build files.
workspace_name (str): The name of the workspace.
render_config: The render config to use.
cargo_lockfile_label (str, optional): The label of the `cargo_lockfile`. Its repository
is where `all_crate_deps(first_party = True)` looks for the Cargo workspace's own
crates.
repository_ctx (repository_ctx, optional): A repository context object
used for enabling certain functionality.

Expand Down Expand Up @@ -378,6 +388,7 @@ def generate_config_file(
render_config = render_config,
supported_platform_triples = supported_platform_triples,
repository_name = repository_name or ctx.label.name,
cargo_lockfile_label = cargo_lockfile_label,
repository_ctx = repository_ctx,
)

Expand Down
18 changes: 16 additions & 2 deletions crate_universe/private/generate_utils.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,15 @@ def _read_cargo_config(repository_ctx):
return repository_ctx.read(config)
return None

def _update_render_config(config, repository_name):
"""Add the repository name to the render config
def _update_render_config(config, repository_name, cargo_lockfile_label):
"""Add rendering details that come from the rule rather than the user

Args:
config (dict): A `render_config` struct
repository_name (str): The name of the repository that owns the config
cargo_lockfile_label (str): The label of the rule's `cargo_lockfile`, or None. Its
repository is what `all_crate_deps(first_party = True)` renders labels against,
since that is the Bazel repository holding the Cargo workspace's own crates.

Returns:
struct: An updated `render_config`.
Expand All @@ -232,6 +235,9 @@ def _update_render_config(config, repository_name):
# Add the repository name as it's very relevant to rendering.
config.update({"repository_name": repository_name})

if cargo_lockfile_label:
config.update({"cargo_lockfile_label": cargo_lockfile_label})

return struct(**config)

def _get_render_config(repository_ctx):
Expand All @@ -256,6 +262,7 @@ def compile_config(
render_config,
supported_platform_triples,
repository_name,
cargo_lockfile_label = None,
repository_ctx = None):
"""Create a config file for generating crate targets

Expand All @@ -271,6 +278,9 @@ def compile_config(
render_config (dict): The deserialized dict of the `render_config` function.
supported_platform_triples (list): A list of platform triples
repository_name (str): The name of the repository being generated
cargo_lockfile_label (str, optional): The label of the rule's `cargo_lockfile`. Used to
locate the Bazel repository that owns the Cargo workspace's own crates when
rendering `all_crate_deps(first_party = True)`.
repository_ctx (repository_ctx, optional): A repository context object used for enabling
certain functionality.

Expand Down Expand Up @@ -310,6 +320,7 @@ def compile_config(
rendering = _update_render_config(
config = render_config,
repository_name = repository_name,
cargo_lockfile_label = cargo_lockfile_label,
),
supported_platform_triples = supported_platform_triples,
)
Expand All @@ -335,6 +346,9 @@ def generate_config(repository_ctx):
render_config = _get_render_config(repository_ctx),
supported_platform_triples = repository_ctx.attr.supported_platform_triples,
repository_name = repository_ctx.name,
# The hub repository's `crates.bzl` lives outside the workspace being
# generated for, so first-party labels need an explicit repository.
cargo_lockfile_label = str(repository_ctx.attr.cargo_lockfile),
repository_ctx = repository_ctx,
)

Expand Down
2 changes: 2 additions & 0 deletions crate_universe/src/api/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ mod test {
got_pkg_a.normal_deps().values(),
vec![
CrateDependency {
workspace_member: false,
id: CrateId {
name: String::from("anyhow"),
version: Version::new(1, 0, 69),
Expand All @@ -179,6 +180,7 @@ mod test {
local_path: None,
},
CrateDependency {
workspace_member: false,
id: CrateId {
name: String::from("reqwest"),
version: Version::new(0, 11, 14),
Expand Down
16 changes: 16 additions & 0 deletions crate_universe/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,21 @@ pub(crate) struct RenderConfig {
/// continue to write subpackage `BUILD.bazel`s into the hub repo directly.
#[serde(default)]
pub(crate) crates_vendor_synthesizes_subpackages: bool,

/// Internal: the label of the rule's `cargo_lockfile`. Only its repository
/// is used, to locate the Bazel repository holding the Cargo workspace's
/// own crates so `all_crate_deps(first_party = True)` can emit labels for
/// them. Injected by the rules, never set by users — a workspace member's
/// Bazel package is already known (`Context::workspace_members`); the
/// repository is the one piece the renderer cannot infer.
///
/// Excluded from the digest by [`crate::lockfile::Digest::new`], mirroring
/// `label_injection_mapping`: canonical repository names are consumer-
/// specific, so hashing this would make a root-level
/// `single_version_override` demand a producer-side repin that a
/// registry-distributed lockfile in a read-only cache can never perform.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) cargo_lockfile_label: Option<Label>,
}

// Default is manually implemented so that the default values match the default
Expand All @@ -156,6 +171,7 @@ impl Default for RenderConfig {
generate_rules_license_metadata: default_generate_rules_license_metadata(),
incompatible_no_root_alias_targets: false,
crates_vendor_synthesizes_subpackages: false,
cargo_lockfile_label: Option::default(),
}
}
}
Expand Down
9 changes: 8 additions & 1 deletion crate_universe/src/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,16 @@ impl Context {
})
.collect::<Result<BTreeMap<CrateId, String>>>()?;

// Workspace members are deliberately absent here: no repository is
// generated for a crate that lives in the Cargo workspace, so listing
// one as a direct dependency would only add noise to the lockfile.
let add_crate_ids = |crates: &mut BTreeSet<CrateId>,
deps: &Select<BTreeSet<Dependency>>| {
for dep in deps.values() {
for dep in deps
.values()
.into_iter()
.filter(|dep| !dep.workspace_member)
{
crates.insert(CrateId::from(
&annotations.metadata.packages[&dep.package_id],
));
Expand Down
9 changes: 8 additions & 1 deletion crate_universe/src/context/crate_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use crate::metadata::{
CrateAnnotation, Dependency, PairedExtras, SourceAnnotation, TreeResolverMetadata,
};
use crate::select::Select;
use crate::utils::sanitize_module_name;
use crate::utils::starlark::{Glob, Label};
use crate::utils::{is_false, sanitize_module_name};

#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct CrateDependency {
Expand All @@ -32,6 +32,12 @@ pub struct CrateDependency {
/// `[dependencies]` table and the `[patches]` table so they can be used in rendering.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub(crate) local_path: Option<Utf8PathBuf>,

/// Whether the dependency is another member of the same Cargo workspace. The
/// rendered dependency maps keep these separate from third-party crates so
/// `all_crate_deps(first_party = True)` can opt into them.
#[serde(default, skip_serializing_if = "is_false")]
pub workspace_member: bool,
}

#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Clone)]
Expand Down Expand Up @@ -409,6 +415,7 @@ impl CrateContext {
id: CrateId::new(pkg.name.clone(), pkg.version.clone()),
target,
alias: dep.alias,
workspace_member: dep.workspace_member,
local_path: match source_annotations.get(&dep.package_id) {
Some(SourceAnnotation::Path { path }) => Some(path.clone()),
_ => None,
Expand Down
4 changes: 4 additions & 0 deletions crate_universe/src/context/platforms.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ mod test {
let mut deps: Select<BTreeSet<CrateDependency>> = Select::default();
deps.insert(
CrateDependency {
workspace_member: false,
id: CrateId::new("mock_crate_b".to_owned(), VERSION_ZERO_ONE_ZERO),
target: "mock_crate_b".to_owned(),
alias: None,
Expand Down Expand Up @@ -191,6 +192,7 @@ mod test {
let mut deps: Select<BTreeSet<CrateDependency>> = Select::default();
deps.insert(
CrateDependency {
workspace_member: false,
id: CrateId::new("mock_crate_b".to_owned(), VERSION_ZERO_ONE_ZERO),
target: "mock_crate_b".to_owned(),
alias: None,
Expand Down Expand Up @@ -278,6 +280,7 @@ mod test {
let mut deps: Select<BTreeSet<CrateDependency>> = Select::default();
deps.insert(
CrateDependency {
workspace_member: false,
id: CrateId::new("mock_crate_b".to_owned(), VERSION_ZERO_ONE_ZERO),
target: "mock_crate_b".to_owned(),
alias: None,
Expand Down Expand Up @@ -345,6 +348,7 @@ mod test {
let mut deps: Select<BTreeSet<CrateDependency>> = Select::default();
deps.insert(
CrateDependency {
workspace_member: false,
id: CrateId::new("mock_crate_b".to_owned(), VERSION_ZERO_ONE_ZERO),
target: "mock_crate_b".to_owned(),
alias: None,
Expand Down
10 changes: 8 additions & 2 deletions crate_universe/src/lockfile.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use regex::Regex;
use serde::{Deserialize, Serialize};
use sha2::{Digest as Sha2Digest, Sha256};

use crate::config::Config;
use crate::config::{Config, RenderConfig};
use crate::context::Context;
use crate::metadata::Cargo;
use crate::splicing::{SplicingManifest, SplicingMetadata};
Expand Down Expand Up @@ -78,9 +78,15 @@ impl Digest {
// `single_version_override` would shift the canonical names, change
// the digest, and force a producer-side repin to recover — which is
// impossible for registry-distributed producers whose lockfile lives
// in a read-only bzlmod cache.
// in a read-only bzlmod cache. `rendering.cargo_lockfile_label` carries
// a consumer-side canonical repository name too, so it is cleared for
// exactly the same reason.
let config_for_hash = Config {
label_injection_mapping: Default::default(),
rendering: RenderConfig {
cargo_lockfile_label: None,
..config.rendering.clone()
},
..config.clone()
};

Expand Down
63 changes: 56 additions & 7 deletions crate_universe/src/metadata/dependency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use serde::{Deserialize, Serialize};
use crate::config::CrateId;
use crate::metadata::TreeResolverMetadata;
use crate::select::Select;
use crate::utils::sanitize_module_name;
use crate::utils::{is_false, sanitize_module_name};

/// A representation of a crate dependency
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
Expand All @@ -25,6 +25,13 @@ pub(crate) struct Dependency {

/// The alias for the dependency from the perspective of the current package
pub(crate) alias: Option<String>,

/// Whether the dependency is another member of the same Cargo workspace.
/// These are tracked but kept out of the third-party dependency maps, since
/// no repository is generated for them; `all_crate_deps(first_party = True)`
/// is what opts into them.
#[serde(default, skip_serializing_if = "is_false")]
pub(crate) workspace_member: bool,
}

/// A collection of [Dependency]s sorted by dependency kind.
Expand All @@ -50,8 +57,6 @@ impl DependencySet {
let (dev, normal) = node
.deps
.iter()
// Do not track workspace members as dependencies. Users are expected to maintain those connections
.filter(|dep| !is_workspace_member(dep, metadata))
.filter(|dep| is_lib_package(&metadata[&dep.pkg]))
.filter(|dep| is_normal_dependency(dep) || is_dev_dependency(dep))
.partition(|dep| is_dev_dependency(dep));
Expand All @@ -72,8 +77,6 @@ impl DependencySet {
let (dev, normal) = node
.deps
.iter()
// Do not track workspace members as dependencies. Users are expected to maintain those connections
.filter(|dep| !is_workspace_member(dep, metadata))
.filter(|dep| is_proc_macro_package(&metadata[&dep.pkg]))
.filter(|dep| is_normal_dependency(dep) || is_dev_dependency(dep))
.partition(|dep| is_dev_dependency(dep));
Expand All @@ -96,8 +99,6 @@ impl DependencySet {
let (proc_macro, normal) = node
.deps
.iter()
// Do not track workspace members as dependencies. Users are expected to maintain those connections
.filter(|dep| !is_workspace_member(dep, metadata))
.filter(|dep| is_build_dependency(dep))
.filter(|dep| !is_dev_dependency(dep))
.partition(|dep| is_proc_macro_package(&metadata[&dep.pkg]));
Expand Down Expand Up @@ -202,6 +203,7 @@ fn collect_deps_selectable(
.expect("Nodes Dependencies are expected to exclusively be library-like targets");
let alias = get_target_alias(&dep.name, dep_pkg);
let crate_id = CrateId::from(dep_pkg);
let workspace_member = is_workspace_member(dep, metadata);

for kind_info in &dep.dep_kinds {
if kind_info.kind != kind {
Expand All @@ -219,6 +221,7 @@ fn collect_deps_selectable(
package_id: dep.pkg.clone(),
target_name: target_name.clone(),
alias: alias.clone(),
workspace_member,
};
select.insert(dependency, config);
}
Expand All @@ -229,6 +232,7 @@ fn collect_deps_selectable(
package_id: dep.pkg.clone(),
target_name: target_name.clone(),
alias: alias.clone(),
workspace_member,
};
select.insert(
dependency,
Expand Down Expand Up @@ -952,4 +956,49 @@ mod test {
"`mio` is a platform specific dependency and therefore should not be identified under the common configuration."
);
}

#[test]
fn workspace_member_deps_are_tracked_and_tagged() {
let metadata = metadata::workspace_path();

let child_b = find_metadata_node("child_b", &metadata);
let depset = DependencySet::new_for_node(child_b, &metadata, None);

// `child_b` depends on `child_a` through `[workspace.dependencies]`.
// The edge is recorded so `all_crate_deps(first_party = True)` has
// something to render, and tagged so the third-party dependency maps
// can leave it out.
let child_a: Vec<_> = depset
.normal_deps
.items()
.into_iter()
.filter(|(_, dep)| dep.target_name == "child_a")
.collect();

assert_eq!(child_a.len(), 1);
let (configuration, dep) = &child_a[0];
assert_eq!(configuration, &None);
assert!(
dep.workspace_member,
"`child_a` is a member of the same Cargo workspace as `child_b`"
);
}

#[test]
fn third_party_deps_are_not_tagged_as_workspace_members() {
let metadata = metadata::common();

let node = find_metadata_node("common", &metadata);
let depset = DependencySet::new_for_node(node, &metadata, None);

assert!(!depset.normal_deps.items().is_empty());
assert!(
depset
.normal_deps
.items()
.iter()
.all(|(_, dep)| !dep.workspace_member),
"no dependency of a single-package workspace can be a workspace member"
);
}
}
Loading
Loading