use std::collections::{BTreeMap, BTreeSet};
use std::fmt::Write;
use super::emit::{BuckEmitter, ConfigName, EmitDeps, EmitInput, EmitOutput, EmitPackage};
pub struct StringTemplateEmitter;
impl BuckEmitter for StringTemplateEmitter {
fn emit(&self, input: &EmitInput) -> EmitOutput {
EmitOutput {
buck: emit_buck(input),
muntjac_bzl: emit_muntjac_bzl(input),
config_buck: emit_config_buck(input),
wiring_bzl: emit_wiring_bzl(input),
}
}
}
fn emit_buck(input: &EmitInput) -> String {
let mut s = String::new();
writeln!(s, "##").unwrap();
writeln!(s, "## @generated by muntjac").unwrap();
writeln!(s, "## Do not edit by hand.").unwrap();
writeln!(s, "##").unwrap();
writeln!(s).unwrap();
writeln!(
s,
"load(\"//{}:muntjac.bzl\", \"pypi_package\")",
input.third_party_dir
)
.unwrap();
let mut sorted_pkgs: Vec<&EmitPackage> = input.packages.iter().collect();
sorted_pkgs.sort_by(|a, b| a.name.cmp(&b.name).then(a.version.cmp(&b.version)));
for pkg in sorted_pkgs {
writeln!(s).unwrap();
write_pypi_package(&mut s, pkg, &input.third_party_dir);
}
s
}
fn emit_muntjac_bzl(input: &EmitInput) -> String {
let mut s = String::new();
let tpd = &input.third_party_dir;
writeln!(s, "##").unwrap();
writeln!(s, "## @generated by muntjac").unwrap();
writeln!(s, "##").unwrap();
writeln!(s, "## Wiring contract for consumers:").unwrap();
writeln!(s, "##").unwrap();
writeln!(
s,
"## One-time project setup — add to your root PACKAGE (see"
)
.unwrap();
writeln!(s, "## {}/wiring.bzl for the full snippet):", tpd).unwrap();
writeln!(s, "##").unwrap();
writeln!(
s,
"## load(\"@prelude//cfg/modifier:set_cfg_modifiers.bzl\", \"set_cfg_modifiers\")"
)
.unwrap();
writeln!(
s,
"## load(\"//{}:wiring.bzl\", \"MUNTJAC_HOST_MODIFIERS\")",
tpd
)
.unwrap();
writeln!(s, "## # ...plus cfg_constructor loads...").unwrap();
writeln!(s, "## set_cfg_constructor(...)").unwrap();
writeln!(
s,
"## set_cfg_modifiers(cfg_modifiers = MUNTJAC_HOST_MODIFIERS)"
)
.unwrap();
writeln!(s, "##").unwrap();
writeln!(
s,
"## This registers buck2's cfg_constructor and auto-routes the host"
)
.unwrap();
writeln!(
s,
"## OS+CPU to the matching muntjac platform constraint."
)
.unwrap();
writeln!(s, "##").unwrap();
writeln!(
s,
"## To pick a python version, set a per-binary modifier or a"
)
.unwrap();
writeln!(s, "## root-PACKAGE default:").unwrap();
writeln!(s, "##").unwrap();
writeln!(s, "## # per-binary").unwrap();
writeln!(s, "## python_binary(").unwrap();
writeln!(s, "## name = \"service\",").unwrap();
writeln!(s, "## modifiers = [\"//{}/config:py312\"],", tpd).unwrap();
writeln!(s, "## deps = [\"//{}:numpy\"],", tpd).unwrap();
writeln!(s, "## main = \"main.py\",").unwrap();
writeln!(s, "## )").unwrap();
writeln!(s, "##").unwrap();
writeln!(s, "## # root PACKAGE default").unwrap();
writeln!(s, "## set_cfg_modifiers([\"//{}/config:py312\"])", tpd).unwrap();
writeln!(s, "##").unwrap();
let py_constraints: BTreeSet<String> = input
.configs
.iter()
.filter_map(|c| c.as_str().split('-').next().map(|p| p.to_string()))
.filter(|p| p.starts_with("py"))
.collect();
let py_list: Vec<&str> = py_constraints.iter().map(|p| p.as_str()).collect();
writeln!(
s,
"## Available muntjac python constraints: {}",
py_list.join(", ")
)
.unwrap();
writeln!(s, "##").unwrap();
writeln!(s).unwrap();
let mut sorted_configs: Vec<&ConfigName> = input.configs.iter().collect();
sorted_configs.sort();
writeln!(s, "_CONFIGS = [").unwrap();
for cfg in &sorted_configs {
writeln!(s, " \"{}\",", cfg).unwrap();
}
writeln!(s, "]").unwrap();
writeln!(s).unwrap();
writeln!(
s,
"def pypi_package(name, version, wheels, deps = [], visibility = None, labels = [], overlay_files = [], entry_points = [], runtime_env = {{}}, **kwargs):"
)
.unwrap();
writeln!(
s,
" unknown = [cfg for cfg in wheels.keys() if cfg not in _CONFIGS]"
)
.unwrap();
writeln!(s, " if unknown:").unwrap();
writeln!(
s,
" fail(\"unknown config(s) in {{}}: {{}}\".format(name, unknown))"
)
.unwrap();
writeln!(s).unwrap();
writeln!(
s,
" # Dedup source-target rules by (src, sha): pure-python wheels often span"
)
.unwrap();
writeln!(
s,
" # all cfgs with the same content. Emit one rule per unique tuple, then"
)
.unwrap();
writeln!(
s,
" # N library variants referencing it. Source-target index assignment is"
)
.unwrap();
writeln!(s, " # deterministic (sorted cfg iteration).").unwrap();
writeln!(s, " seen_sources = {{}}").unwrap();
writeln!(s, " src_targets = {{}}").unwrap();
writeln!(s, " for cfg in sorted(wheels.keys()):").unwrap();
writeln!(s, " src, sha256 = wheels[cfg]").unwrap();
writeln!(s, " sha = sha256.removeprefix(\"sha256:\")").unwrap();
writeln!(s, " key = (src, sha)").unwrap();
writeln!(s, " if key in seen_sources:").unwrap();
writeln!(s, " src_targets[cfg] = seen_sources[key]").unwrap();
writeln!(s, " continue").unwrap();
writeln!(
s,
" target = \"{{}}-{{}}-src-{{}}\".format(name, version, len(seen_sources))"
)
.unwrap();
writeln!(s, " seen_sources[key] = target").unwrap();
writeln!(s, " src_targets[cfg] = target").unwrap();
writeln!(s).unwrap();
writeln!(s, " if src.startswith(\"prebake:\"):").unwrap();
writeln!(s, " rel = src[len(\"prebake:\"):]").unwrap();
writeln!(s, " native.export_file(").unwrap();
writeln!(s, " name = target,").unwrap();
writeln!(s, " src = \"prebake/{{}}\".format(rel),").unwrap();
writeln!(s, " visibility = [],").unwrap();
writeln!(s, " )").unwrap();
writeln!(s, " else:").unwrap();
writeln!(s, " native.http_file(").unwrap();
writeln!(s, " name = target,").unwrap();
writeln!(s, " sha256 = sha,").unwrap();
writeln!(s, " urls = [src],").unwrap();
writeln!(s, " visibility = [],").unwrap();
writeln!(s, " )").unwrap();
writeln!(s).unwrap();
writeln!(
s,
" # Overlay (S6): if overlay_files non-empty, build an `__overlaid` rule"
)
.unwrap();
writeln!(
s,
" # that unzips the wheel, copies overlay sources in, and re-zips."
)
.unwrap();
writeln!(
s,
" # The result is a single .whl that replaces every per-cell source."
)
.unwrap();
writeln!(s, " overlay_label = None").unwrap();
writeln!(s, " if overlay_files:").unwrap();
writeln!(s, " first_src = sorted(src_targets.values())[0]").unwrap();
writeln!(
s,
" # Wrap each overlay file in an export_file so genrule's $(location ...)"
)
.unwrap();
writeln!(
s,
" # can resolve to it (buck2 rejects raw file paths inside $(location))."
)
.unwrap();
writeln!(s, " overlay_targets = []").unwrap();
writeln!(
s,
" for (i, (path_in_wheel, src_label)) in enumerate(overlay_files):"
)
.unwrap();
writeln!(
s,
" t = \"{{}}-{{}}__overlay-src-{{}}\".format(name, version, i)"
)
.unwrap();
writeln!(s, " native.export_file(").unwrap();
writeln!(s, " name = t,").unwrap();
writeln!(s, " src = src_label,").unwrap();
writeln!(s, " visibility = [],").unwrap();
writeln!(s, " )").unwrap();
writeln!(s, " overlay_targets.append((path_in_wheel, t))").unwrap();
writeln!(s, " cp_lines = []").unwrap();
writeln!(s, " for (path_in_wheel, t) in overlay_targets:").unwrap();
writeln!(s, " parent = path_in_wheel.rsplit(\"/\", 1)[0] if \"/\" in path_in_wheel else \".\"").unwrap();
writeln!(s, " cp_lines.append(\"mkdir -p _u/\" + parent + \" && cp $(location :\" + t + \") _u/\" + path_in_wheel)").unwrap();
writeln!(s, " cp_cmds = \" && \".join(cp_lines)").unwrap();
writeln!(
s,
" # Genrule cmd runs at the project root. Buck's $(location :X)"
)
.unwrap();
writeln!(
s,
" # expands to a project-root-relative path, so unzip must NOT cd"
)
.unwrap();
writeln!(
s,
" # into _u before invoking it — instead, use `unzip -d _u`. The"
)
.unwrap();
writeln!(
s,
" # final zip is run from inside _u so the archive's entries don't"
)
.unwrap();
writeln!(s, " # carry the `_u/` prefix.").unwrap();
writeln!(s, " cmd_template = (").unwrap();
writeln!(s, " \"set -e && mkdir _u && unzip -q -d _u $(location :\" + first_src + \") && \" +").unwrap();
writeln!(
s,
" cp_cmds + \" && cd _u && zip -qrX ../$OUT . -x '*/RECORD'\""
)
.unwrap();
writeln!(s, " )").unwrap();
writeln!(s, " native.genrule(").unwrap();
writeln!(
s,
" name = \"{{}}-{{}}__overlaid\".format(name, version),"
)
.unwrap();
writeln!(
s,
" srcs = [\":\" + first_src] + [\":\" + t for (_, t) in overlay_targets],"
)
.unwrap();
writeln!(
s,
" out = \"{{}}-{{}}-overlaid.whl\".format(name, version),"
)
.unwrap();
writeln!(s, " cmd = cmd_template,").unwrap();
writeln!(s, " visibility = [],").unwrap();
writeln!(s, " )").unwrap();
writeln!(
s,
" overlay_label = \":{{}}-{{}}__overlaid\".format(name, version)"
)
.unwrap();
writeln!(s).unwrap();
writeln!(s, " for cfg in sorted(wheels.keys()):").unwrap();
writeln!(s, " native.prebuilt_python_library(").unwrap();
writeln!(
s,
" name = \"{{}}-{{}}__{{}}\".format(name, version, cfg),"
)
.unwrap();
writeln!(
s,
" binary_src = overlay_label or \":{{}}\".format(src_targets[cfg]),"
)
.unwrap();
writeln!(s, " deps = deps,").unwrap();
writeln!(s, " visibility = [],").unwrap();
writeln!(s, " )").unwrap();
writeln!(s).unwrap();
writeln!(s, " native.alias(").unwrap();
writeln!(s, " name = \"{{}}-{{}}\".format(name, version),").unwrap();
writeln!(s, " actual = select({{").unwrap();
writeln!(
s,
" \"//{}/config:{{}}\".format(cfg): \":{{}}-{{}}__{{}}\".format(name, version, cfg)",
tpd
)
.unwrap();
writeln!(s, " for cfg in wheels.keys()").unwrap();
writeln!(s, " }}),").unwrap();
writeln!(s, " visibility = [],").unwrap();
writeln!(s, " )").unwrap();
writeln!(s, " native.alias(").unwrap();
writeln!(s, " name = name,").unwrap();
writeln!(s, " actual = \":{{}}-{{}}\".format(name, version),").unwrap();
writeln!(s, " visibility = visibility or [\"PUBLIC\"],").unwrap();
writeln!(s, " )").unwrap();
writeln!(s).unwrap();
writeln!(
s,
" # Entry points (S6): one python_binary + convenience alias per name."
)
.unwrap();
writeln!(
s,
" # main_module uses the standard `<importable>.__main__` convention;"
)
.unwrap();
writeln!(
s,
" # entry points that need a different module require a wheel-meta shim"
)
.unwrap();
writeln!(s, " # (deferred — see TECH_DEBT TD-S6-03).").unwrap();
writeln!(s, " for ep_name in entry_points:").unwrap();
writeln!(s, " importable = name.replace(\"-\", \"_\")").unwrap();
writeln!(s, " native.python_binary(").unwrap();
writeln!(
s,
" name = \"{{}}-{{}}__bin-{{}}\".format(name, version, ep_name),"
)
.unwrap();
writeln!(s, " main_module = importable + \".__main__\",").unwrap();
writeln!(s, " deps = [\":\" + name],").unwrap();
writeln!(s, " env = runtime_env,").unwrap();
writeln!(s, " visibility = visibility or [\"PUBLIC\"],").unwrap();
writeln!(s, " labels = labels,").unwrap();
writeln!(s, " )").unwrap();
writeln!(s, " native.alias(").unwrap();
writeln!(s, " name = ep_name,").unwrap();
writeln!(
s,
" actual = \":{{}}-{{}}__bin-{{}}\".format(name, version, ep_name),"
)
.unwrap();
writeln!(s, " visibility = visibility or [\"PUBLIC\"],").unwrap();
writeln!(s, " )").unwrap();
s
}
fn emit_config_buck(input: &EmitInput) -> String {
let mut s = String::new();
writeln!(s, "##").unwrap();
writeln!(s, "## @generated by muntjac").unwrap();
writeln!(s, "## Do not edit by hand.").unwrap();
writeln!(s, "##").unwrap();
writeln!(s).unwrap();
let mut pythons: BTreeSet<&str> = BTreeSet::new();
let mut platforms: BTreeSet<&str> = BTreeSet::new();
for cfg in &input.configs {
let s_cfg = cfg.as_str();
if let Some(dash) = s_cfg.find('-') {
pythons.insert(&s_cfg[..dash]);
platforms.insert(&s_cfg[dash + 1..]);
}
}
writeln!(s, "constraint_setting(name = \"python_version\")").unwrap();
for py in &pythons {
writeln!(s, "constraint_value(name = \"{}\", constraint_setting = \":python_version\", visibility = [\"PUBLIC\"])", py).unwrap();
}
writeln!(s).unwrap();
writeln!(s, "constraint_setting(name = \"platform\")").unwrap();
for plat in &platforms {
writeln!(s, "constraint_value(name = \"{}\", constraint_setting = \":platform\", visibility = [\"PUBLIC\"])", plat).unwrap();
}
writeln!(s).unwrap();
let mut sorted_configs: Vec<&ConfigName> = input.configs.iter().collect();
sorted_configs.sort();
for cell in &sorted_configs {
let s_cell = cell.as_str();
let dash = s_cell
.find('-')
.expect("ConfigName format py<X>-<platform>");
let py = &s_cell[..dash];
let plat = &s_cell[dash + 1..];
let mut cvs = vec![plat, py];
cvs.sort();
writeln!(s, "config_setting(").unwrap();
writeln!(s, " name = \"{}\",", cell).unwrap();
writeln!(s, " constraint_values = [").unwrap();
for cv in &cvs {
writeln!(s, " \":{}\",", cv).unwrap();
}
writeln!(s, " ],").unwrap();
writeln!(s, " visibility = [\"PUBLIC\"],").unwrap();
writeln!(s, ")").unwrap();
}
s
}
fn emit_wiring_bzl(input: &EmitInput) -> String {
let mut s = String::new();
writeln!(s, "##").unwrap();
writeln!(s, "## @generated by muntjac").unwrap();
writeln!(s, "## Do not edit by hand.").unwrap();
writeln!(s, "##").unwrap();
writeln!(
s,
"## Host-axis modifiers for muntjac's per-cell platform constraints."
)
.unwrap();
writeln!(s, "##").unwrap();
writeln!(
s,
"## The user's root PACKAGE loads MUNTJAC_HOST_MODIFIERS and passes it"
)
.unwrap();
writeln!(
s,
"## to set_cfg_modifiers() directly. set_cfg_modifiers() cannot be wrapped"
)
.unwrap();
writeln!(
s,
"## in a helper function — the prelude enforces that it be called from"
)
.unwrap();
writeln!(s, "## a PACKAGE/BUCK_TREE file, not a .bzl file.").unwrap();
writeln!(s, "##").unwrap();
writeln!(
s,
"## Wiring contract for consumers — add to your root PACKAGE:"
)
.unwrap();
writeln!(s, "##").unwrap();
writeln!(s, "## load(").unwrap();
writeln!(
s,
"## \"@prelude//cfg/modifier:cfg_constructor.bzl\","
)
.unwrap();
writeln!(s, "## \"cfg_constructor_post_constraint_analysis\",").unwrap();
writeln!(s, "## \"cfg_constructor_pre_constraint_analysis\",").unwrap();
writeln!(s, "## )").unwrap();
writeln!(
s,
"## load(\"@prelude//cfg/modifier:common.bzl\", \"MODIFIER_METADATA_KEY\")"
)
.unwrap();
writeln!(
s,
"## load(\"@prelude//cfg/modifier:set_cfg_modifiers.bzl\", \"set_cfg_modifiers\")"
)
.unwrap();
writeln!(
s,
"## load(\"//{}:wiring.bzl\", \"MUNTJAC_HOST_MODIFIERS\")",
input.third_party_dir
)
.unwrap();
writeln!(s, "##").unwrap();
writeln!(s, "## set_cfg_constructor(").unwrap();
writeln!(
s,
"## stage0 = cfg_constructor_pre_constraint_analysis,"
)
.unwrap();
writeln!(
s,
"## stage1 = cfg_constructor_post_constraint_analysis,"
)
.unwrap();
writeln!(s, "## key = MODIFIER_METADATA_KEY,").unwrap();
writeln!(s, "## aliases = struct(),").unwrap();
writeln!(s, "## extra_data = struct(),").unwrap();
writeln!(s, "## )").unwrap();
writeln!(
s,
"## set_cfg_modifiers(cfg_modifiers = MUNTJAC_HOST_MODIFIERS)"
)
.unwrap();
writeln!(s, "##").unwrap();
writeln!(s).unwrap();
let mut platforms: BTreeSet<&str> = BTreeSet::new();
for cfg in &input.configs {
if let Some((_, plat)) = cfg.as_str().split_once('-') {
platforms.insert(plat);
}
}
let mut by_os: BTreeMap<&str, Vec<(&str, &str)>> = BTreeMap::new();
for p in &platforms {
let parts: Vec<&str> = p.split('-').collect();
if parts.len() < 2 {
continue;
}
let os = parts[0];
if p.ends_with("-musl") {
continue;
}
let cpu_constraint = match parts[1] {
"aarch64" => "arm64",
other => other,
};
by_os.entry(os).or_default().push((cpu_constraint, *p));
}
let total_branches: usize = by_os.values().map(|v| v.len()).sum();
if total_branches == 0 {
writeln!(s, "MUNTJAC_HOST_MODIFIERS = []").unwrap();
return s;
}
writeln!(s, "MUNTJAC_HOST_MODIFIERS = [").unwrap();
writeln!(s, " {{").unwrap();
writeln!(s, " \"_type\": \"ModifiersMatch\",").unwrap();
for (os, cpus) in &by_os {
let os_key = format!("prelude//os/constraints:{}", os);
writeln!(s, " \"{}\": {{", os_key).unwrap();
writeln!(s, " \"_type\": \"ModifiersMatch\",").unwrap();
let mut cpus_sorted: Vec<&(&str, &str)> = cpus.iter().collect();
cpus_sorted.sort();
for (cpu, plat_name) in cpus_sorted {
let cpu_key = format!("prelude//cpu/constraints:{}", cpu);
let target = format!("root//{}/config:{}", input.third_party_dir, plat_name);
writeln!(s, " \"{}\": \"{}\",", cpu_key, target).unwrap();
}
writeln!(s, " }},").unwrap();
}
writeln!(s, " }},").unwrap();
writeln!(s, "]").unwrap();
s
}
fn write_pypi_package(s: &mut String, pkg: &EmitPackage, pkg_third_party_dir: &str) {
writeln!(s, "pypi_package(").unwrap();
writeln!(s, " name = \"{}\",", pkg.name).unwrap();
writeln!(s, " version = \"{}\",", pkg.version).unwrap();
match &pkg.deps {
EmitDeps::Uniform(v) => {
if v.is_empty() {
writeln!(s, " deps = [],").unwrap();
} else {
writeln!(s, " deps = [").unwrap();
for dep in v {
writeln!(s, " \"{}\",", dep).unwrap();
}
writeln!(s, " ],").unwrap();
}
}
EmitDeps::PerCell(per_cell) => {
writeln!(s, " deps = select({{").unwrap();
for (cell, deps_v) in per_cell {
let key = format!("//{}/config:{}", pkg_third_party_dir, cell);
if deps_v.is_empty() {
writeln!(s, " \"{}\": [],", key).unwrap();
} else {
writeln!(s, " \"{}\": [", key).unwrap();
for d in deps_v {
writeln!(s, " \"{}\",", d).unwrap();
}
writeln!(s, " ],").unwrap();
}
}
writeln!(s, " }}),").unwrap();
}
}
writeln!(s, " wheels = {{").unwrap();
for (cfg, wheel) in &pkg.wheels {
writeln!(
s,
" \"{}\": (\"{}\", \"{}\"),",
cfg, wheel.url, wheel.hash
)
.unwrap();
}
writeln!(s, " }},").unwrap();
if let Some(overlay) = &pkg.overlay {
writeln!(s, " overlay_files = [").unwrap();
for (in_wheel, src_rel) in &overlay.files {
writeln!(s, " (\"{}\", \"{}\"),", in_wheel, src_rel).unwrap();
}
writeln!(s, " ],").unwrap();
}
if !pkg.entry_points.is_empty() {
writeln!(s, " entry_points = [").unwrap();
for ep in &pkg.entry_points {
writeln!(s, " \"{}\",", ep).unwrap();
}
writeln!(s, " ],").unwrap();
}
if !pkg.labels.is_empty() {
writeln!(s, " labels = [").unwrap();
for l in &pkg.labels {
writeln!(s, " \"{}\",", l).unwrap();
}
writeln!(s, " ],").unwrap();
}
if !pkg.runtime_env.is_empty() {
writeln!(s, " runtime_env = {{").unwrap();
for (k, v) in &pkg.runtime_env {
writeln!(s, " \"{}\": \"{}\",", k, v).unwrap();
}
writeln!(s, " }},").unwrap();
}
if let Some(vis) = &pkg.visibility {
writeln!(s, " visibility = [").unwrap();
for v in vis {
writeln!(s, " \"{}\",", v).unwrap();
}
writeln!(s, " ],").unwrap();
} else {
writeln!(s, " visibility = [\"PUBLIC\"],").unwrap();
}
writeln!(s, ")").unwrap();
}
#[cfg(test)]
mod tests {
use super::*;
use crate::buck::emit::ConfigName;
use crate::buck::emit::{EmitPackage, EmitWheel};
use std::collections::BTreeMap;
fn empty_input() -> EmitInput {
EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![ConfigName::new("3.12", "linux-x86_64-gnu")],
packages: vec![],
}
}
fn single_package_input() -> EmitInput {
let cfg = ConfigName::new("3.12", "linux-x86_64-gnu");
let mut wheels = BTreeMap::new();
wheels.insert(
cfg.clone(),
EmitWheel {
url: "https://files.pythonhosted.org/p/certifi-2025.4.26-py3-none-any.whl".into(),
hash: "sha256:0123abcd".into(),
},
);
EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![cfg],
packages: vec![EmitPackage {
name: "certifi".into(),
version: "2025.4.26".into(),
deps: EmitDeps::Uniform(vec![]),
wheels,
overlay: None,
entry_points: vec![],
visibility: None,
labels: vec![],
runtime_env: std::collections::BTreeMap::new(),
}],
}
}
#[test]
fn emitter_returns_four_strings() {
let out = StringTemplateEmitter.emit(&empty_input());
let _ = out.buck;
let _ = out.muntjac_bzl;
let _ = out.config_buck;
let _ = out.wiring_bzl;
}
#[test]
fn empty_input_buck_has_header_and_load() {
let out = StringTemplateEmitter.emit(&empty_input());
assert!(
out.buck
.starts_with("##\n## @generated by muntjac\n## Do not edit by hand.\n##\n\n"),
"missing or malformed BUCK header:\n{}",
out.buck
);
assert!(
out.buck
.contains(r#"load("//third-party/python:muntjac.bzl", "pypi_package")"#),
"missing or wrong load() line:\n{}",
out.buck
);
}
#[test]
fn single_package_buck_contains_pypi_package_call() {
let out = StringTemplateEmitter.emit(&single_package_input());
assert!(out.buck.contains("pypi_package("));
assert!(out.buck.contains(r#"name = "certifi""#));
assert!(out.buck.contains(r#"version = "2025.4.26""#));
assert!(out.buck.contains(r#""py312-linux-x86_64-gnu":"#));
assert!(out.buck.contains("sha256:0123abcd"));
assert!(out.buck.contains(r#"visibility = ["PUBLIC"]"#));
}
#[test]
fn muntjac_bzl_has_header_and_configs_list() {
let out = StringTemplateEmitter.emit(&empty_input());
assert!(
out.muntjac_bzl
.starts_with("##\n## @generated by muntjac\n##\n"),
"missing muntjac.bzl header:\n{}",
out.muntjac_bzl
);
assert!(
out.muntjac_bzl
.contains("## Wiring contract for consumers:"),
"missing wiring contract block:\n{}",
out.muntjac_bzl
);
assert!(out.muntjac_bzl.contains("MUNTJAC_HOST_MODIFIERS"));
assert!(out.muntjac_bzl.contains("set_cfg_modifiers"));
assert!(out.muntjac_bzl.contains("_CONFIGS = ["));
assert!(out.muntjac_bzl.contains(" \"py312-linux-x86_64-gnu\","));
assert!(out.muntjac_bzl.contains("def pypi_package("));
assert!(out.muntjac_bzl.contains("native.prebuilt_python_library"));
assert!(out.muntjac_bzl.contains("native.http_file"));
assert!(!out.muntjac_bzl.contains("expect("));
assert!(out.muntjac_bzl.contains("if unknown:"));
assert!(out.muntjac_bzl.contains("//third-party/python/config:"));
}
#[test]
fn muntjac_bzl_configs_list_sorted_lex() {
let mut input = empty_input();
input.configs = vec![
ConfigName::new("3.12", "linux-x86_64-gnu"),
ConfigName::new("3.11", "linux-x86_64-gnu"),
];
let out = StringTemplateEmitter.emit(&input);
let p311 = out
.muntjac_bzl
.find("\"py311-linux-x86_64-gnu\"")
.expect("py311 missing");
let p312 = out
.muntjac_bzl
.find("\"py312-linux-x86_64-gnu\"")
.expect("py312 missing");
assert!(p311 < p312, "_CONFIGS not sorted:\n{}", out.muntjac_bzl);
}
#[test]
fn config_buck_declares_per_cell_settings() {
let mut input = empty_input();
input.configs = vec![
ConfigName::new("3.11", "linux-x86_64-gnu"),
ConfigName::new("3.12", "linux-x86_64-gnu"),
];
let out = StringTemplateEmitter.emit(&input);
assert!(
out.config_buck
.starts_with("##\n## @generated by muntjac\n## Do not edit by hand.\n##\n\n"),
"missing config/BUCK header:\n{}",
out.config_buck
);
assert!(out.config_buck.contains("config_setting("));
assert!(
out.config_buck
.contains(r#"name = "py311-linux-x86_64-gnu""#)
);
assert!(
out.config_buck
.contains(r#"name = "py312-linux-x86_64-gnu""#)
);
assert!(
out.config_buck
.contains(r#"constraint_setting(name = "python_version")"#)
);
assert!(
out.config_buck
.contains(r#"constraint_value(name = "py311""#)
);
assert!(
out.config_buck
.contains(r#"constraint_value(name = "py312""#)
);
assert!(
out.config_buck
.contains(r#"constraint_setting(name = "platform")"#)
);
assert!(
out.config_buck
.contains(r#"constraint_value(name = "linux-x86_64-gnu""#)
);
}
#[test]
fn config_buck_settings_sorted_by_name() {
let mut input = empty_input();
input.configs = vec![
ConfigName::new("3.12", "linux-x86_64-gnu"),
ConfigName::new("3.11", "linux-x86_64-gnu"),
];
let out = StringTemplateEmitter.emit(&input);
let p311 = out
.config_buck
.find(r#"name = "py311-linux-x86_64-gnu""#)
.expect("py311 missing");
let p312 = out
.config_buck
.find(r#"name = "py312-linux-x86_64-gnu""#)
.expect("py312 missing");
assert!(
p311 < p312,
"config_settings must be sorted:\n{}",
out.config_buck
);
}
#[test]
fn wiring_bzl_has_header_and_host_modifiers() {
let out = StringTemplateEmitter.emit(&empty_input());
assert!(
out.wiring_bzl
.starts_with("##\n## @generated by muntjac\n## Do not edit by hand.\n##\n"),
"missing wiring.bzl header:\n{}",
out.wiring_bzl
);
assert!(
out.wiring_bzl.contains("MUNTJAC_HOST_MODIFIERS"),
"wiring.bzl should export MUNTJAC_HOST_MODIFIERS"
);
assert!(
out.wiring_bzl.contains("set_cfg_modifiers"),
"wiring.bzl docstring should reference set_cfg_modifiers"
);
assert!(
out.wiring_bzl.contains("set_cfg_constructor"),
"wiring.bzl docstring should reference set_cfg_constructor"
);
}
#[test]
fn two_packages_emitted_alphabetically() {
let mut input = single_package_input();
let mut wheels = BTreeMap::new();
wheels.insert(
input.configs[0].clone(),
EmitWheel {
url: "https://example.com/alpha-1.0-py3-none-any.whl".into(),
hash: "sha256:aaaa".into(),
},
);
input.packages.insert(
0,
EmitPackage {
name: "alpha".into(),
version: "1.0".into(),
deps: EmitDeps::Uniform(vec![]),
wheels,
overlay: None,
entry_points: vec![],
visibility: None,
labels: vec![],
runtime_env: std::collections::BTreeMap::new(),
},
);
let out = StringTemplateEmitter.emit(&input);
let alpha_pos = out
.buck
.find("name = \"alpha\"")
.expect("alpha not emitted");
let certifi_pos = out
.buck
.find("name = \"certifi\"")
.expect("certifi not emitted");
assert!(
alpha_pos < certifi_pos,
"alpha must precede certifi in output:\n{}",
out.buck
);
}
fn multi_package_input() -> EmitInput {
let cfg_311 = ConfigName::new("3.11", "linux-x86_64-gnu");
let cfg_312 = ConfigName::new("3.12", "linux-x86_64-gnu");
let mut requests_wheels = BTreeMap::new();
requests_wheels.insert(
cfg_311.clone(),
EmitWheel {
url: "https://example.com/requests-2.32.3-py3-none-any.whl".into(),
hash: "sha256:rrrr".into(),
},
);
requests_wheels.insert(
cfg_312.clone(),
EmitWheel {
url: "https://example.com/requests-2.32.3-py3-none-any.whl".into(),
hash: "sha256:rrrr".into(),
},
);
let mut idna_wheels = BTreeMap::new();
idna_wheels.insert(
cfg_311.clone(),
EmitWheel {
url: "https://example.com/idna-3.7-py3-none-any.whl".into(),
hash: "sha256:iiii".into(),
},
);
idna_wheels.insert(
cfg_312.clone(),
EmitWheel {
url: "https://example.com/idna-3.7-py3-none-any.whl".into(),
hash: "sha256:iiii".into(),
},
);
EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![cfg_311, cfg_312],
packages: vec![
EmitPackage {
name: "idna".into(),
version: "3.7".into(),
deps: EmitDeps::Uniform(vec![]),
wheels: idna_wheels,
overlay: None,
entry_points: vec![],
visibility: None,
labels: vec![],
runtime_env: std::collections::BTreeMap::new(),
},
EmitPackage {
name: "requests".into(),
version: "2.32.3".into(),
deps: EmitDeps::Uniform(vec![":idna".into()]),
wheels: requests_wheels,
overlay: None,
entry_points: vec![],
visibility: None,
labels: vec![],
runtime_env: std::collections::BTreeMap::new(),
},
],
}
}
#[test]
fn snapshot_empty_buck() {
insta::assert_snapshot!(StringTemplateEmitter.emit(&empty_input()).buck);
}
#[test]
fn snapshot_empty_muntjac_bzl() {
insta::assert_snapshot!(StringTemplateEmitter.emit(&empty_input()).muntjac_bzl);
}
#[test]
fn snapshot_empty_config_buck() {
insta::assert_snapshot!(StringTemplateEmitter.emit(&empty_input()).config_buck);
}
#[test]
fn wiring_bzl_emits_host_modifiers_for_three_platforms() {
use crate::buck::emit::{ConfigName, EmitInput};
let input = EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![
ConfigName::new("3.11", "linux-aarch64-gnu"),
ConfigName::new("3.11", "linux-x86_64-gnu"),
ConfigName::new("3.11", "macos-arm64"),
ConfigName::new("3.12", "linux-aarch64-gnu"),
ConfigName::new("3.12", "linux-x86_64-gnu"),
ConfigName::new("3.12", "macos-arm64"),
],
packages: vec![],
};
let out = StringTemplateEmitter.emit(&input);
insta::assert_snapshot!("wiring_bzl_three_platforms", out.wiring_bzl);
}
#[test]
fn wiring_bzl_emits_host_modifiers_for_single_platform() {
use crate::buck::emit::{ConfigName, EmitInput};
let input = EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![
ConfigName::new("3.11", "linux-x86_64-gnu"),
ConfigName::new("3.12", "linux-x86_64-gnu"),
],
packages: vec![],
};
let out = StringTemplateEmitter.emit(&input);
insta::assert_snapshot!("wiring_bzl_single_platform", out.wiring_bzl);
}
#[test]
fn wiring_bzl_emits_empty_host_modifiers_for_musllinux_only() {
use crate::buck::emit::{ConfigName, EmitInput};
let input = EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![ConfigName::new("3.12", "linux-x86_64-musl")],
packages: vec![],
};
let out = StringTemplateEmitter.emit(&input);
insta::assert_snapshot!("wiring_bzl_musllinux_only", out.wiring_bzl);
}
#[test]
fn snapshot_single_package_buck() {
insta::assert_snapshot!(StringTemplateEmitter.emit(&single_package_input()).buck);
}
#[test]
fn snapshot_multi_package_buck() {
insta::assert_snapshot!(StringTemplateEmitter.emit(&multi_package_input()).buck);
}
#[test]
fn snapshot_multi_package_muntjac_bzl() {
insta::assert_snapshot!(
StringTemplateEmitter
.emit(&multi_package_input())
.muntjac_bzl
);
}
#[test]
fn snapshot_multi_package_config_buck() {
insta::assert_snapshot!(
StringTemplateEmitter
.emit(&multi_package_input())
.config_buck
);
}
#[test]
fn renders_per_cell_deps_as_select() {
let cells: Vec<ConfigName> = vec![
ConfigName::new("3.11", "linux-x86_64-gnu"),
ConfigName::new("3.12", "linux-x86_64-gnu"),
];
let mut per_cell: BTreeMap<ConfigName, Vec<String>> = BTreeMap::new();
per_cell.insert(
cells[0].clone(),
vec![":foo".into(), ":typing-extensions".into()],
);
per_cell.insert(cells[1].clone(), vec![":foo".into()]);
let mut wheels: BTreeMap<ConfigName, EmitWheel> = BTreeMap::new();
for cell in &cells {
wheels.insert(
cell.clone(),
EmitWheel {
url: format!("https://example.com/rich-13.0-{}.whl", cell),
hash: "sha256:rich".into(),
},
);
}
let input = EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: cells,
packages: vec![EmitPackage {
name: "rich".into(),
version: "13.0".into(),
deps: EmitDeps::PerCell(per_cell),
wheels,
overlay: None,
entry_points: vec![],
visibility: None,
labels: vec![],
runtime_env: std::collections::BTreeMap::new(),
}],
};
let out = StringTemplateEmitter.emit(&input);
insta::assert_snapshot!("per_cell_deps_select", out.buck);
}
#[test]
fn muntjac_bzl_emits_full_macro_with_header() {
let input = EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![
ConfigName::new("3.11", "linux-x86_64-gnu"),
ConfigName::new("3.12", "linux-x86_64-gnu"),
],
packages: vec![],
};
let out = StringTemplateEmitter.emit(&input);
insta::assert_snapshot!("muntjac_bzl_with_header_and_native", out.muntjac_bzl);
}
#[test]
fn renders_uniform_deps_as_plain_list() {
let cell = ConfigName::new("3.12", "linux-x86_64-gnu");
let mut wheels: BTreeMap<ConfigName, EmitWheel> = BTreeMap::new();
wheels.insert(
cell.clone(),
EmitWheel {
url: "https://example.com/requests-2.32.3-py3-none-any.whl".into(),
hash: "sha256:req".into(),
},
);
let input = EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![cell],
packages: vec![EmitPackage {
name: "requests".into(),
version: "2.32.3".into(),
deps: EmitDeps::Uniform(vec![":certifi".into(), ":idna".into()]),
wheels,
overlay: None,
entry_points: vec![],
visibility: None,
labels: vec![],
runtime_env: std::collections::BTreeMap::new(),
}],
};
let out = StringTemplateEmitter.emit(&input);
insta::assert_snapshot!("uniform_deps_plain_list", out.buck);
}
#[test]
fn config_buck_emits_multi_platform_constraints() {
let input = EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![
ConfigName::new("3.11", "linux-aarch64-gnu"),
ConfigName::new("3.11", "linux-x86_64-gnu"),
ConfigName::new("3.11", "macos-arm64"),
ConfigName::new("3.12", "linux-aarch64-gnu"),
ConfigName::new("3.12", "linux-x86_64-gnu"),
ConfigName::new("3.12", "macos-arm64"),
],
packages: vec![],
};
let out = StringTemplateEmitter.emit(&input);
insta::assert_snapshot!("config_buck_six_cells", out.config_buck);
}
#[test]
fn buck_render_emits_prebake_url_verbatim() {
let cfg = ConfigName::new("3.12", "linux-x86_64-gnu");
let mut wheels = BTreeMap::new();
wheels.insert(
cfg.clone(),
EmitWheel {
url: "prebake:tomli-2.0.1-py3-none-any.whl".to_string(),
hash: "sha256:cafef00d".to_string(),
},
);
let input = EmitInput {
tree: "default".to_string(),
third_party_dir: "third-party/python".to_string(),
configs: vec![cfg],
packages: vec![EmitPackage {
name: "tomli".to_string(),
version: "2.0.1".to_string(),
deps: EmitDeps::Uniform(vec![]),
wheels,
overlay: None,
entry_points: vec![],
visibility: None,
labels: vec![],
runtime_env: std::collections::BTreeMap::new(),
}],
};
let writer = StringTemplateEmitter;
let out = writer.emit(&input);
assert!(
out.buck
.contains("\"prebake:tomli-2.0.1-py3-none-any.whl\""),
"BUCK output should contain prebake URL verbatim:\n{}",
out.buck
);
}
#[test]
fn macro_definition_includes_overlay_and_entry_points_branches() {
let input = empty_input(); let out = StringTemplateEmitter.emit(&input);
assert!(
out.muntjac_bzl.contains("overlay_files = []"),
"macro signature should include overlay_files = [] kwarg"
);
assert!(
out.muntjac_bzl.contains("entry_points = []"),
"macro signature should include entry_points = [] kwarg"
);
assert!(
out.muntjac_bzl.contains("labels = []"),
"macro signature should include labels = [] kwarg"
);
assert!(
out.muntjac_bzl.contains("runtime_env"),
"macro signature should include runtime_env kwarg"
);
assert!(
out.muntjac_bzl.contains("if overlay_files"),
"expected overlay branch (`if overlay_files:`)"
);
assert!(
out.muntjac_bzl.contains("native.genrule"),
"expected native.genrule for overlay"
);
assert!(
out.muntjac_bzl.contains("unzip"),
"expected unzip in overlay cmd"
);
assert!(
out.muntjac_bzl.contains("zip -qrX"),
"expected zip -qrX in overlay cmd (deterministic, strips timestamps)"
);
assert!(
out.muntjac_bzl.contains("RECORD"),
"expected RECORD exclusion in overlay cmd"
);
assert!(
out.muntjac_bzl.contains("for ep_name in entry_points"),
"expected entry_points loop"
);
assert!(
out.muntjac_bzl.contains("native.python_binary"),
"expected python_binary for entry points"
);
assert!(
out.muntjac_bzl.contains(".__main__"),
"expected __main__ convention in main_module"
);
}
#[test]
fn per_package_call_site_renders_non_default_kwargs_only() {
use crate::buck::emit::{EmitDeps, EmitOverlay, EmitPackage, EmitWheel};
let cfg = ConfigName::new("3.12", "linux-x86_64-gnu");
let mut wheels = BTreeMap::new();
wheels.insert(
cfg.clone(),
EmitWheel {
url: "https://example.com/fake-pillow-1.0-py3-none-any.whl".into(),
hash: "sha256:fakehash".into(),
},
);
let mut runtime_env = BTreeMap::new();
runtime_env.insert("LIBJPEG_PATH".into(), "/opt/libjpeg/lib".into());
let input = EmitInput {
tree: "default".into(),
third_party_dir: "third-party/python".into(),
configs: vec![cfg],
packages: vec![EmitPackage {
name: "fake-pillow".into(),
version: "1.0.0".into(),
deps: EmitDeps::Uniform(vec![]),
wheels,
overlay: Some(EmitOverlay {
files: vec![(
"fake_pillow/_paths.py".into(),
"fixups/fake-pillow/overlay/fake_pillow/_paths.py".into(),
)],
}),
entry_points: vec!["fake-pillow-cli".into()],
visibility: Some(vec!["//apps/imaging/...".into()]),
labels: vec!["security-sensitive".into()],
runtime_env,
}],
};
let out = StringTemplateEmitter.emit(&input);
assert!(
out.buck.contains("overlay_files = ["),
"missing overlay_files kwarg:\n{}",
out.buck
);
assert!(
out.buck.contains("\"fake_pillow/_paths.py\""),
"missing overlay in_wheel path"
);
assert!(
out.buck
.contains("\"fixups/fake-pillow/overlay/fake_pillow/_paths.py\""),
"missing overlay src path"
);
assert!(
out.buck.contains("entry_points = ["),
"missing entry_points kwarg"
);
assert!(
out.buck.contains("\"fake-pillow-cli\""),
"missing entry-point name"
);
assert!(
out.buck.contains("visibility = ["),
"missing visibility kwarg"
);
assert!(
out.buck.contains("\"//apps/imaging/...\""),
"missing visibility entry"
);
assert!(out.buck.contains("labels = ["), "missing labels kwarg");
assert!(out.buck.contains("\"security-sensitive\""), "missing label");
assert!(
out.buck.contains("runtime_env = {"),
"missing runtime_env kwarg"
);
assert!(
out.buck.contains("\"LIBJPEG_PATH\""),
"missing runtime_env key"
);
assert!(
out.buck.contains("\"/opt/libjpeg/lib\""),
"missing runtime_env value"
);
}
#[test]
fn per_package_call_site_omits_default_kwargs() {
let input = single_package_input();
let out = StringTemplateEmitter.emit(&input);
assert!(
!out.buck.contains("overlay_files"),
"should not emit overlay_files when None"
);
assert!(
!out.buck.contains("entry_points = ["),
"should not emit entry_points kwarg when empty"
);
assert!(
!out.buck.contains("labels = ["),
"should not emit labels kwarg when empty"
);
assert!(
!out.buck.contains("runtime_env = {"),
"should not emit runtime_env kwarg when empty"
);
}
#[test]
fn macro_overlay_rebinds_binary_src() {
let input = empty_input();
let out = StringTemplateEmitter.emit(&input);
assert!(
out.muntjac_bzl.contains("overlay_label or "),
"binary_src must conditionally use overlay_label:\n{}",
out.muntjac_bzl
);
}
}