use deno_core::{JsRuntimeForSnapshot, RuntimeOptions};
use semver::Version;
use serde_json::Value as JsonValue;
use std::fs::read_to_string;
use std::path::PathBuf;
use std::{env, error::Error, fs, io::Write, path::Path, process::Command};
use toml_edit::{value as new_toml_value, Document as TomlDocument};
fn main() {
let out_dir = std::env::var_os("OUT_DIR").expect("$OUT_DIR not set.");
println!("cargo:rerun-if-changed={:?}", &out_dir);
let out_dir: PathBuf = out_dir.into();
if cfg!(target_arch = "musl") {
panic!("This package cannot be built for musl architectures.");
}
let current_dir = std::env::current_dir().unwrap();
if std::fs::metadata("./package.json").is_ok() {
update_manifests();
bundle_for_deno(¤t_dir);
}
create_snapshot(&out_dir).expect("unable to create v8 snapshot: composition.snap");
}
fn bundle_for_deno(current_dir: &Path) {
let npm = which::which("npm").expect("You must have npm installed to build this crate.");
if cfg!(debug_assertions) {
println!(
"cargo:warning=running `npm install` in {}",
¤t_dir.display()
);
assert!(Command::new(&npm)
.current_dir(current_dir)
.args(["install"])
.status()
.expect("Could not get status of `npm install`")
.success());
} else {
println!(
"cargo:warning=running `npm ci` in {}",
¤t_dir.display()
);
assert!(Command::new(&npm)
.current_dir(current_dir)
.args(["ci"])
.status()
.expect("Could not get status of `npm ci`")
.success());
}
println!(
"cargo:warning=running `npm run format` in {}",
¤t_dir.display()
);
assert!(Command::new(&npm)
.current_dir(current_dir)
.args(["run", "format"])
.status()
.expect("Could not get status of `npm run format`")
.success());
println!(
"cargo:warning=running `npm run build` in {}",
¤t_dir.display()
);
assert!(Command::new(&npm)
.current_dir(current_dir)
.args(["run", "build"])
.status()
.expect("Could not get status of `npm run build`")
.success());
}
fn update_manifests() {
let current_dir = std::env::current_dir().expect("Could not find the current directory.");
let harmonizer_manifest_path = current_dir.join("Cargo.toml");
let maybe_harmonizer_version = update_this_manifest(&harmonizer_manifest_path);
if let Some(harmonizer_version) = maybe_harmonizer_version {
println!(
"cargo:warning=updated {} to {}",
&harmonizer_manifest_path.display(),
&harmonizer_version
);
let federation_workspace_dir = current_dir
.parent()
.expect("Could not find parent directory.");
let supergraph_dir = federation_workspace_dir.join("supergraph");
let supergraph_manifest_path = supergraph_dir.join("Cargo.toml");
update_supergraph_manifest(&supergraph_manifest_path, &harmonizer_version);
}
}
fn update_this_manifest(build_manifest_path: &Path) -> Option<Version> {
let build_manifest_contents =
fs::read_to_string(build_manifest_path).expect("Could not read 'Cargo.toml'");
let mut build_manifest = build_manifest_contents
.parse::<TomlDocument>()
.expect("Cargo.toml is not valid TOML");
let js_composition_version = get_underlying_composition_npm_module_version();
let crate_version = Version::parse(
build_manifest["package"]["version"]
.as_str()
.expect("`package.version` in Cargo.toml is not a string"),
)
.expect("Crate version is not valid semver");
if js_composition_version != crate_version {
build_manifest["package"]["version"] = new_toml_value(js_composition_version.to_string());
fs::write(build_manifest_path, build_manifest.to_string())
.expect("Could not write updated Cargo.toml");
Some(js_composition_version)
} else {
None
}
}
fn update_supergraph_manifest(supergraph_manifest_path: &Path, new_package_version: &Version) {
let supergraph_manifest_contents =
fs::read_to_string(supergraph_manifest_path).expect("Could not read Cargo.toml");
let mut supergraph_manifest = supergraph_manifest_contents
.parse::<TomlDocument>()
.expect("Cargo.toml is not valid TOML");
supergraph_manifest["package"]["version"] = new_toml_value(new_package_version.to_string());
fs::write(supergraph_manifest_path, supergraph_manifest.to_string())
.expect("Could not update Cargo.toml");
}
fn get_underlying_composition_npm_module_version() -> Version {
let current_dir = env::current_dir().unwrap();
let npm_manifest_path = current_dir.join("package.json");
let mut npm_manifest_contents: JsonValue = serde_json::from_str(
&fs::read_to_string(&npm_manifest_path).expect("Could not read package.json"),
)
.expect("package.json is not valid JSON");
let maybe_federation = npm_manifest_contents["dependencies"]["@apollo/federation"].as_str();
let maybe_composition = npm_manifest_contents["dependencies"]["@apollo/composition"].as_str();
let (dep_name, version_string) = match (maybe_federation, maybe_composition) {
(None, Some(composition)) => {
let dep_name = "@apollo/composition".to_string();
let version_str = npm_manifest_contents["dependencies"][&dep_name]
.as_str()
.unwrap_or_else(|| panic!("`.dependencies.{}` is not a string", &composition));
(dep_name, version_str.to_string())
}
(Some(federation), None) => {
let dep_name = "@apollo/federation".to_string();
let version_str = npm_manifest_contents["dependencies"][&dep_name]
.as_str()
.unwrap_or_else(|| panic!("`.dependencies.{}` is not a string", &federation));
(dep_name, version_str.to_string())
}
(Some(_federation), Some(_composition)) => unreachable!(
"Found both `@apollo/federation` and `@apollo/composition`. There should only be one."
),
(None, None) => unreachable!(
"Underlying npm module must be either `@apollo/federation` or `@apollo/composition`"
),
};
let parsed_version = Version::parse(&version_string).unwrap_or_else(|_| {
panic!(
"version for `{}`, `{}`, is not valid semver",
&dep_name, &version_string
)
});
npm_manifest_contents["version"] = JsonValue::from(version_string);
fs::write(
&npm_manifest_path,
serde_json::to_string_pretty(&npm_manifest_contents).expect("Could not pretty print JSON"),
)
.expect("Could not write updated contents to package.json");
parsed_version
}
#[cfg(all(target_os = "macos", target_arch = "x86_64"))]
fn create_snapshot(out_dir: &Path) -> Result<(), Box<dyn Error>> {
Ok(())
}
#[cfg(not(all(target_os = "macos", target_arch = "x86_64")))]
fn create_snapshot(out_dir: &Path) -> Result<(), Box<dyn Error>> {
let options = RuntimeOptions {
..Default::default()
};
let mut runtime = JsRuntimeForSnapshot::new(options);
let runtime_str = read_to_string("bundled/runtime.js").unwrap();
runtime
.execute_script("<init>", deno_core::FastString::Owned(runtime_str.into()))
.expect("unable to initialize router bridge runtime environment");
let bridge_str = read_to_string("bundled/composition_bridge.js").unwrap();
runtime
.execute_script(
"composition_bridge.js",
deno_core::FastString::Owned(bridge_str.into()),
)
.expect("unable to evaluate bridge module");
println!("cargo:warning={:?}", &out_dir);
let mut snap = fs::File::create(out_dir.join("composition.snap"))?;
snap.write_all(&runtime.snapshot())?;
Ok(())
}