blob: 5b6bd77ffda37dad226ead4b29ebf6f96484ce7d [file] [log] [blame]
"""Utilities directly related to the `generate` step of `cargo-bazel`."""
load(":common_utils.bzl", "CARGO_BAZEL_ISOLATED", "REPIN_ENV_VARS", "cargo_environ", "execute")
CARGO_BAZEL_GENERATOR_SHA256 = "CARGO_BAZEL_GENERATOR_SHA256"
CARGO_BAZEL_GENERATOR_URL = "CARGO_BAZEL_GENERATOR_URL"
GENERATOR_ENV_VARS = [
CARGO_BAZEL_GENERATOR_URL,
CARGO_BAZEL_GENERATOR_SHA256,
]
CRATES_REPOSITORY_ENVIRON = GENERATOR_ENV_VARS + REPIN_ENV_VARS + [
CARGO_BAZEL_ISOLATED,
]
def get_generator(repository_ctx, host_triple):
"""Query network resources to locate a `cargo-bazel` binary
Args:
repository_ctx (repository_ctx): The rule's context object.
host_triple (string): A string representing the host triple
Returns:
tuple(path, dict): The path to a `cargo-bazel` binary and the host sha256 pairing.
The pairing (dict) may be `None` if there is no need to update the attribute
"""
use_environ = False
for var in GENERATOR_ENV_VARS:
if var in repository_ctx.os.environ:
use_environ = True
output = repository_ctx.path("cargo-bazel.exe" if "win" in repository_ctx.os.name else "cargo-bazel")
# The `generator` attribute is the next highest priority behind
# environment variables. We check those first before deciding to
# use an explicitly provided variable.
if not use_environ and repository_ctx.attr.generator:
generator = repository_ctx.path(Label(repository_ctx.attr.generator))
# Resolve a few levels of symlinks to ensure we're accessing the direct binary
for _ in range(1, 100):
real_generator = generator.realpath
if real_generator == generator:
break
generator = real_generator
return generator, None
# The environment variable will take precedence if set
if use_environ:
generator_sha256 = repository_ctx.os.environ.get(CARGO_BAZEL_GENERATOR_SHA256)
generator_url = repository_ctx.os.environ.get(CARGO_BAZEL_GENERATOR_URL)
else:
generator_sha256 = repository_ctx.attr.generator_sha256s.get(host_triple)
generator_url = repository_ctx.attr.generator_urls.get(host_triple)
if not generator_url:
fail((
"No generator URL was found either in the `CARGO_BAZEL_GENERATOR_URL` " +
"environment variable or for the `{}` triple in the `generator_urls` attribute"
).format(host_triple))
# Download the file into place
if generator_sha256:
repository_ctx.download(
output = output,
url = generator_url,
sha256 = generator_sha256,
executable = True,
)
return output, None
result = repository_ctx.download(
output = output,
url = generator_url,
executable = True,
)
return output, {host_triple: result.sha256}
def render_config(
build_file_template = "//:BUILD.{name}-{version}.bazel",
crate_label_template = "@{repository}__{name}-{version}//:{target}",
crate_repository_template = "{repository}__{name}-{version}",
crates_module_template = "//:{file}",
default_package_name = None,
platforms_template = "@rules_rust//rust/platform:{triple}",
regen_command = None,
vendor_mode = None):
"""Various settings used to configure rendered outputs
The template parameters each support a select number of format keys. A description of each key
can be found below where the supported keys for each template can be found in the parameter docs
| key | definition |
| --- | --- |
| `name` | The name of the crate. Eg `tokio` |
| `repository` | The rendered repository name for the crate. Directly relates to `crate_repository_template`. |
| `triple` | A platform triple. Eg `x86_64-unknown-linux-gnu` |
| `version` | The crate version. Eg `1.2.3` |
| `target` | The library or binary target of the crate |
| `file` | The basename of a file |
Args:
build_file_template (str, optional): The base template to use for BUILD file names. The available format keys
are [`{name}`, {version}`].
crate_label_template (str, optional): The base template to use for crate labels. The available format keys
are [`{repository}`, `{name}`, `{version}`, `{target}`].
crate_repository_template (str, optional): The base template to use for Crate label repository names. The
available format keys are [`{repository}`, `{name}`, `{version}`].
crates_module_template (str, optional): The pattern to use for the `defs.bzl` and `BUILD.bazel`
file names used for the crates module. The available format keys are [`{file}`].
default_package_name (str, optional): The default package name to use in the rendered macros. This affects the
auto package detection of things like `all_crate_deps`.
platforms_template (str, optional): The base template to use for platform names.
See [platforms documentation](https://docs.bazel.build/versions/main/platforms.html). The available format
keys are [`{triple}`].
regen_command (str, optional): An optional command to demonstrate how generated files should be regenerated.
vendor_mode (str, optional): An optional configuration for rendirng content to be rendered into repositories.
Returns:
string: A json encoded struct to match the Rust `config::RenderConfig` struct
"""
return json.encode(struct(
build_file_template = build_file_template,
crate_label_template = crate_label_template,
crate_repository_template = crate_repository_template,
crates_module_template = crates_module_template,
default_package_name = default_package_name,
platforms_template = platforms_template,
regen_command = regen_command,
vendor_mode = vendor_mode,
))
def _crate_id(name, version):
"""Creates a `cargo_bazel::config::CrateId`.
Args:
name (str): The name of the crate
version (str): The crate's version
Returns:
str: A serialized representation of a CrateId
"""
return "{} {}".format(name, version)
def collect_crate_annotations(annotations, repository_name):
"""Deserialize and sanitize crate annotations.
Args:
annotations (dict): A mapping of crate names to lists of serialized annotations
repository_name (str): The name of the repository that owns the annotations
Returns:
dict: A mapping of `cargo_bazel::config::CrateId` to sets of annotations
"""
annotations = {name: [json.decode(a) for a in annotation] for name, annotation in annotations.items()}
crate_annotations = {}
for name, annotation in annotations.items():
for (version, data) in annotation:
if name == "*" and version != "*":
fail(
"Wildcard crate names must have wildcard crate versions. " +
"Please update the `annotations` attribute of the {} crates_repository".format(
repository_name,
),
)
id = _crate_id(name, version)
if id in crate_annotations:
fail("Found duplicate entries for {}".format(id))
crate_annotations.update({id: data})
return crate_annotations
def _read_cargo_config(repository_ctx):
if repository_ctx.attr.cargo_config:
config = repository_ctx.path(repository_ctx.attr.cargo_config)
return repository_ctx.read(config)
return None
def _update_render_config(config, repository_name):
"""Add the repository name to the render config
Args:
config (dict): A `render_config` struct
repository_name (str): The name of the repository that owns the config
Returns:
struct: An updated `render_config`.
"""
# Add the repository name as it's very relevant to rendering.
config.update({"repository_name": repository_name})
return struct(**config)
def _get_render_config(repository_ctx):
if repository_ctx.attr.render_config:
config = dict(json.decode(repository_ctx.attr.render_config))
else:
config = dict(json.decode(render_config()))
if not config.get("regen_command"):
config["regen_command"] = "bazel sync --only={}".format(
repository_ctx.name,
)
return config
def compile_config(crate_annotations, generate_build_scripts, cargo_config, render_config, supported_platform_triples, repository_name, repository_ctx = None):
"""Create a config file for generating crate targets
[cargo_config]: https://doc.rust-lang.org/cargo/reference/config.html
Args:
crate_annotations (dict): Extra settings to apply to crates. See
`crates_repository.annotations` or `crates_vendor.annotations`.
generate_build_scripts (bool): Whether or not to globally disable build scripts.
cargo_config (str): The optional contents of a [Cargo config][cargo_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
repository_ctx (repository_ctx, optional): A repository context object used for enabling
certain functionality.
Returns:
struct: A struct matching a `cargo_bazel::config::Config`.
"""
annotations = collect_crate_annotations(crate_annotations, repository_name)
# Load additive build files if any have been provided.
unexpected = []
for name, data in annotations.items():
f = data.pop("additive_build_file", None)
if f and not repository_ctx:
unexpected.append(name)
f = None
content = [x for x in [
data.pop("additive_build_file_content", None),
repository_ctx.read(Label(f)) if f else None,
] if x]
if content:
data.update({"additive_build_file_content": "\n".join(content)})
if unexpected:
fail("The following annotations use `additive_build_file` which is not supported for {}: {}".format(repository_name, unexpected))
config = struct(
generate_build_scripts = generate_build_scripts,
annotations = annotations,
cargo_config = cargo_config,
rendering = _update_render_config(
config = render_config,
repository_name = repository_name,
),
supported_platform_triples = supported_platform_triples,
)
return config
def generate_config(repository_ctx):
"""Generate a config file from various attributes passed to the rule.
Args:
repository_ctx (repository_ctx): The rule's context object.
Returns:
struct: A struct containing the path to a config and it's contents
"""
config = compile_config(
crate_annotations = repository_ctx.attr.annotations,
generate_build_scripts = repository_ctx.attr.generate_build_scripts,
cargo_config = _read_cargo_config(repository_ctx),
render_config = _get_render_config(repository_ctx),
supported_platform_triples = repository_ctx.attr.supported_platform_triples,
repository_name = repository_ctx.name,
repository_ctx = repository_ctx,
)
config_path = repository_ctx.path("cargo-bazel.json")
repository_ctx.file(
config_path,
json.encode_indent(config, indent = " " * 4),
)
return config_path
def get_lockfiles(repository_ctx):
"""_summary_
Args:
repository_ctx (repository_ctx): The rule's context object.
Returns:
struct: _description_
"""
return struct(
cargo = repository_ctx.path(repository_ctx.attr.cargo_lockfile),
bazel = repository_ctx.path(repository_ctx.attr.lockfile) if repository_ctx.attr.lockfile else None,
)
def determine_repin(repository_ctx, generator, lockfile_path, config, splicing_manifest, cargo, rustc):
"""Use the `cargo-bazel` binary to determine whether or not dpeendencies need to be re-pinned
Args:
repository_ctx (repository_ctx): The rule's context object.
generator (path): The path to a `cargo-bazel` binary.
config (path): The path to a `cargo-bazel` config file. See `generate_config`.
splicing_manifest (path): The path to a `cargo-bazel` splicing manifest. See `create_splicing_manifest`
lockfile_path (path): The path to a "lock" file for reproducible outputs.
cargo (path): The path to a Cargo binary.
rustc (path): The path to a Rustc binary.
Returns:
bool: True if dependencies need to be re-pinned
"""
# If a repin environment variable is set, always repin
for var in REPIN_ENV_VARS:
if var in repository_ctx.os.environ and repository_ctx.os.environ[var].lower() not in ["false", "no", "0", "off"]:
return True
# If a deterministic lockfile was not added then always repin
if not lockfile_path or lockfile_path.exists:
return True
# Run the binary to check if a repin is needed
args = [
generator,
"query",
"--lockfile",
lockfile_path,
"--config",
config,
"--splicing-manifest",
splicing_manifest,
"--cargo",
cargo,
"--rustc",
rustc,
]
env = {
"CARGO": str(cargo),
"RUSTC": str(rustc),
"RUST_BACKTRACE": "full",
}
# Add any Cargo environment variables to the `cargo-bazel` execution
env.update(cargo_environ(repository_ctx))
result = execute(
repository_ctx = repository_ctx,
args = args,
env = env,
)
# If it was determined repinning should occur but there was no
# flag indicating repinning was requested, an error is raised
# since repinning should be an explicit action
if result.stdout.strip().lower() == "repin":
# buildifier: disable=print
print(result.stderr)
fail((
"The current `lockfile` is out of date for '{}'. Please re-run " +
"bazel using `CARGO_BAZEL_REPIN=true` if this is expected " +
"and the lockfile should be updated."
).format(repository_ctx.name))
return False
def execute_generator(
repository_ctx,
lockfile_path,
cargo_lockfile_path,
generator,
config,
splicing_manifest,
repository_dir,
cargo,
rustc,
metadata = None):
"""Execute the `cargo-bazel` binary to produce `BUILD` and `.bzl` files.
Args:
repository_ctx (repository_ctx): The rule's context object.
lockfile_path (path): The path to a "lock" file (file used for reproducible renderings).
cargo_lockfile_path (path): The path to a "Cargo.lock" file within the root workspace.
generator (path): The path to a `cargo-bazel` binary.
config (path): The path to a `cargo-bazel` config file.
splicing_manifest (path): The path to a `cargo-bazel` splicing manifest. See `create_splicing_manifest`
repository_dir (path): The output path for the Bazel module and BUILD files.
cargo (path): The path of a Cargo binary.
rustc (path): The path of a Rustc binary.
metadata (path, optional): The path to a Cargo metadata json file. If this is set, it indicates to
the generator that repinning is required. This file must be adjacent to a `Cargo.toml` and
`Cargo.lock` file.
Returns:
struct: The results of `repository_ctx.execute`.
"""
repository_ctx.report_progress("Generating crate BUILD files.")
args = [
generator,
"generate",
"--cargo-lockfile",
cargo_lockfile_path,
"--config",
config,
"--splicing-manifest",
splicing_manifest,
"--repository-dir",
repository_dir,
"--cargo",
cargo,
"--rustc",
rustc,
]
if lockfile_path:
args.extend([
"--lockfile",
lockfile_path,
])
env = {
"RUST_BACKTRACE": "full",
}
# Some components are not required unless re-pinning is enabled
if metadata:
args.extend([
"--repin",
"--metadata",
metadata,
])
env.update({
"CARGO": str(cargo),
"RUSTC": str(rustc),
})
# Add any Cargo environment variables to the `cargo-bazel` execution
env.update(cargo_environ(repository_ctx))
result = execute(
repository_ctx = repository_ctx,
args = args,
env = env,
)
return result