use axoasset::{LocalAsset, SourceFile};
use camino::{Utf8Path, Utf8PathBuf};
use cargo_dist_schema::{ArtifactId, GlibcVersion, TripleName};
use serde::Serialize;
use super::InstallerInfo;
use crate::{
backend::templates::{
Templates, TEMPLATE_INSTALLER_NPM, TEMPLATE_INSTALLER_NPM_PACKAGE_JSON,
TEMPLATE_INSTALLER_NPM_RUN_JS, TEMPLATE_INSTALLER_NPM_SHRINKWRAP,
},
errors::DistResult,
platform::LibcVersion,
DistGraph, SortedMap, SortedSet,
};
#[derive(Debug, Clone, Serialize)]
pub struct NpmInstallerInfo {
pub npm_package_name: String,
pub npm_package_version: String,
pub npm_package_desc: Option<String>,
pub npm_package_repository_url: Option<String>,
pub npm_package_homepage_url: Option<String>,
pub npm_package_authors: Vec<String>,
pub npm_package_license: Option<String>,
pub npm_package_keywords: Option<Vec<String>>,
pub package_dir: Utf8PathBuf,
pub inner: InstallerInfo,
pub create_shrinkwrap: bool,
}
const RUN_JS: &str = "run.js";
pub(crate) const PACKAGE_JSON: &str = "package.json";
pub(crate) const PACKAGE_LOCK: &str = "npm-shrinkwrap.json";
type PackageJsonPlatforms = SortedMap<TripleName, PackageJsonPlatform>;
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct PackageJsonPlatform {
artifact_name: ArtifactId,
bins: SortedMap<String, String>,
zip_ext: String,
}
#[derive(Debug, Clone, Default)]
struct PlatformSummary {
bins: SortedSet<String>,
aliases: SortedMap<String, String>,
platforms: PackageJsonPlatforms,
}
#[derive(Serialize, Debug, Clone)]
struct RunInfo {
bin: String,
}
pub(crate) fn write_npm_project(dist: &DistGraph, info: &NpmInstallerInfo) -> DistResult<()> {
let templates = &dist.templates;
let mut files = templates.render_dir_to_clean_strings(TEMPLATE_INSTALLER_NPM, info)?;
let platforms = platforms(info);
mangle_run_js(templates, &platforms, &mut files)?;
mangle_package_json(templates, info, &platforms, &mut files)?;
if info.create_shrinkwrap {
mangle_package_lock(templates, info, &platforms, &mut files)?;
}
let zip_dir = &info.package_dir;
for (relpath, rendered) in files {
LocalAsset::write_new_all(&rendered, zip_dir.join(relpath))?;
}
Ok(())
}
fn mangle_run_js(
templates: &Templates,
platforms: &PlatformSummary,
files: &mut SortedMap<Utf8PathBuf, String>,
) -> DistResult<()> {
let run_js_path = Utf8Path::new(RUN_JS);
files
.remove(run_js_path)
.expect("npm template didn't have a run.js!?");
for bin in &platforms.bins {
let filename = platforms.run_js_name_for_bin(bin);
let info = RunInfo {
bin: bin.to_owned(),
};
let contents =
templates.render_file_to_clean_string(TEMPLATE_INSTALLER_NPM_RUN_JS, &info)?;
files.insert(Utf8PathBuf::from(filename), contents);
}
Ok(())
}
fn mangle_package_lock(
templates: &Templates,
info: &NpmInstallerInfo,
platforms: &PlatformSummary,
files: &mut SortedMap<Utf8PathBuf, String>,
) -> DistResult<()> {
let package_lock_path = Utf8Path::new(PACKAGE_LOCK);
let orig_package_lock =
templates.render_file_to_clean_string(TEMPLATE_INSTALLER_NPM_SHRINKWRAP, &info)?;
let package_lock_src = SourceFile::new(PACKAGE_LOCK, orig_package_lock);
let mut package_lock = package_lock_src.deserialize_json::<serde_json::Value>()?;
package_lock["name"] = info.npm_package_name.clone().into();
package_lock["version"] = info.npm_package_version.clone().into();
let root_package = &mut package_lock["packages"][""];
root_package["name"] = info.npm_package_name.clone().into();
root_package["version"] = info.npm_package_version.clone().into();
if let Some(val) = info.npm_package_license.clone() {
root_package["license"] = val.into();
}
root_package["bin"] = platforms.bins_json();
let new_package_lock = serde_json::to_string_pretty(&package_lock).expect("serde_json failed");
files.insert(package_lock_path.to_owned(), new_package_lock);
Ok(())
}
fn mangle_package_json(
templates: &Templates,
info: &NpmInstallerInfo,
platforms: &PlatformSummary,
files: &mut SortedMap<Utf8PathBuf, String>,
) -> DistResult<()> {
let package_json_path = Utf8Path::new(PACKAGE_JSON);
let orig_package_json =
templates.render_file_to_clean_string(TEMPLATE_INSTALLER_NPM_PACKAGE_JSON, &info)?;
let package_json_src = SourceFile::new(PACKAGE_JSON, orig_package_json);
let mut package_json = package_json_src.deserialize_json::<serde_json::Value>()?;
package_json["name"] = info.npm_package_name.clone().into();
package_json["version"] = info.npm_package_version.clone().into();
if let Some(val) = info.npm_package_desc.clone() {
package_json["description"] = val.into();
}
if let Some(val) = info.npm_package_repository_url.clone() {
package_json["repository"] = val.into();
}
if let Some(val) = info.npm_package_homepage_url.clone() {
package_json["homepage"] = val.into();
}
if let Some(val) = info.npm_package_license.clone() {
package_json["license"] = val.into();
}
if let Some(val) = info.npm_package_keywords.clone() {
package_json["keywords"] = val.into();
}
if info.npm_package_authors.len() > 1 {
package_json["contributors"] = info.npm_package_authors.clone().into();
} else if !info.npm_package_authors.is_empty() {
package_json["author"] = info.npm_package_authors[0].clone().into();
}
package_json["bin"] = platforms.bins_json();
package_json["artifactDownloadUrls"] = info.inner.base_urls.clone().into();
package_json["supportedPlatforms"] = platforms.platform_support_json();
match info.inner.runtime_conditions.min_glibc_version {
Some(LibcVersion { major, series }) => {
package_json["glibcMinimum"] = glibc_json(major, series)
}
_ => {
let default = GlibcVersion::default();
package_json["glibcMinimum"] = glibc_json(default.major, default.series)
}
}
let new_package_json = serde_json::to_string_pretty(&package_json).expect("serde_json failed");
files.insert(package_json_path.to_owned(), new_package_json);
Ok(())
}
fn glibc_json(major: u64, series: u64) -> serde_json::Value {
let mut map = SortedMap::<&str, u64>::new();
map.insert("major", major);
map.insert("series", series);
serde_json::to_value(&map).expect("serde_json failed")
}
fn platforms(info: &NpmInstallerInfo) -> PlatformSummary {
let mut output = PlatformSummary::default();
for archive in &info.inner.artifacts {
let target = archive.target_triple.clone();
let mut bins = SortedMap::new();
for bin in &archive.executables {
let raw_name = bin.strip_suffix(".exe").unwrap_or(bin);
bins.insert(raw_name.to_owned(), bin.to_owned());
output.bins.insert(raw_name.to_owned());
let Some(alias_map) = info.inner.bin_aliases.get(&target) else {
continue;
};
let Some(aliases) = alias_map.get(bin) else {
continue;
};
for alias in aliases {
let raw_alias_name = alias.strip_suffix(".exe").unwrap_or(alias);
output
.aliases
.insert(raw_alias_name.to_owned(), raw_name.to_owned());
}
}
let platform = PackageJsonPlatform {
artifact_name: archive.id.clone(),
bins,
zip_ext: archive.zip_style.ext().to_owned(),
};
output.platforms.insert(target, platform);
}
output
}
impl PlatformSummary {
fn bins_json(&self) -> serde_json::Value {
let mut bins = SortedMap::<String, String>::new();
for bin in &self.bins {
let path = self.run_js_name_for_bin(bin);
bins.insert(bin.to_owned(), path);
}
for (alias, bin) in &self.aliases {
let path = self.run_js_name_for_bin(bin);
bins.insert(alias.to_owned(), path);
}
serde_json::to_value(&bins).expect("serde_json failed")
}
fn platform_support_json(&self) -> serde_json::Value {
serde_json::to_value(&self.platforms).expect("serde_json failed")
}
fn run_js_name_for_bin(&self, bin: &str) -> String {
format!("run-{bin}.js")
}
}