diff --git a/Cargo.lock b/Cargo.lock index c024374..c821fe0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -765,6 +765,18 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-xml-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65162e9059be2f6a3421ebbb4fef3e74b7d9e7c60c50a0e292c6239f19f1edfa" +dependencies = [ + "log", + "serde", + "thiserror", + "xml-rs", +] + [[package]] name = "serde_derive" version = "1.0.197" @@ -843,6 +855,7 @@ dependencies = [ "insta-cmd", "semver", "serde", + "serde-xml-rs", "serde_json", "sha2", "tar", @@ -1244,6 +1257,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml-rs" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c5b940ebc25896e71dd073bad2dbaa2abfe97b0a391415e22ad1326d9c54e3c4" + [[package]] name = "zeroize" version = "1.7.0" diff --git a/Cargo.toml b/Cargo.toml index 3e6c9dc..4fb9d2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,13 +6,13 @@ repository = "https://github.com/asg017/sqlite-dist" [dependencies] -base16ct = {version="0.2.0", features=["alloc"]} +base16ct = { version = "0.2.0", features = ["alloc"] } base64 = "0.21.7" chrono = "0.4.34" clap = "4.5.1" flate2 = "1.0.28" -semver = {version="1.0.22", features = ["serde"]} -serde = {version="1.0", features = ["derive"]} +semver = { version = "1.0.22", features = ["serde"] } +serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sha2 = "0.10.8" tar = "0.4.40" @@ -20,6 +20,7 @@ thiserror = "1.0.57" toml = "0.8.10" ureq = "2.9.6" zip = "0.6.6" +serde-xml-rs = "0.5.1" [profile.dist] inherits = "release" diff --git a/src/main.rs b/src/main.rs index cd7d80d..a9918e1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,6 +4,7 @@ mod gh_releases; mod installer_sh; mod manifest; mod npm; +mod nuget; mod pip; mod spec; mod spm; @@ -138,6 +139,7 @@ enum GeneratedAssetKind { Spm, Amalgamation, Manifest, + Nuget, } impl ToString for GeneratedAssetKind { @@ -154,6 +156,7 @@ impl ToString for GeneratedAssetKind { GeneratedAssetKind::Spm => "spm".to_owned(), GeneratedAssetKind::Amalgamation => "amalgamation".to_owned(), GeneratedAssetKind::Manifest => "sqlite-dist-manifest".to_owned(), + GeneratedAssetKind::Nuget => "nuget".to_owned(), } } } @@ -541,6 +544,14 @@ fn build(matches: ArgMatches) -> Result<(), BuildError> { std::fs::create_dir(&gem_path)?; generated_assets.extend(gem::write_gems(&project, &gem_path, gem_config)?); }; + if project.spec.targets.nuget.is_some() { + let nuget_output_directory = output_dir.join("nuget"); + std::fs::create_dir(&nuget_output_directory)?; + generated_assets.extend(nuget::write_nuget_packages( + &project, + &nuget_output_directory, + )?); + } let github_releases_checksums_txt = generated_assets .iter() diff --git a/src/nuget.rs b/src/nuget.rs new file mode 100644 index 0000000..e1728eb --- /dev/null +++ b/src/nuget.rs @@ -0,0 +1,288 @@ +use crate::{Cpu, GeneratedAsset, GeneratedAssetKind, Os, Project}; +use serde::{Deserialize, Serialize}; +use std::{ + io::{self, Cursor, Write}, + path::Path, +}; +use zip::{write::FileOptions, ZipWriter}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct Nuspec { + pub metadata: Metadata, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Metadata { + pub id: String, + pub version: String, + pub title: String, + pub authors: String, + pub owners: String, + pub require_license_acceptance: bool, + pub description: String, + pub files: Option>, +} + +impl Nuspec { + fn new(project: &Project) -> Self { + let author = project.spec.package.authors.first().unwrap(); + Self { + metadata: Metadata { + id: project.spec.package.name.clone(), + version: project.version.to_string(), + title: project.spec.package.name.clone(), + authors: author.clone(), + owners: author.clone(), + require_license_acceptance: false, + description: project.spec.package.description.clone(), + files: None, + }, + } + } + + fn to_xml(&self) -> String { + let files_xml = if let Some(files) = &self.metadata.files { + files + .iter() + .map(|file| format!("", file, file)) + .collect::>() + .join("\n") + } else { + String::new() + }; + + format!( + r#" + + + {} + {} + {} + {} + {} + {} + {} + + + {} + +"#, + self.metadata.id, + self.metadata.version, + self.metadata.title, + self.metadata.authors, + self.metadata.owners, + self.metadata.require_license_acceptance, + self.metadata.description, + files_xml + ) + } +} + +pub struct NugetPackage { + zip: ZipWriter>>, +} + +impl NugetPackage { + pub fn new(package_id: &str) -> io::Result { + let buffer = Vec::new(); + let cursor = Cursor::new(buffer); + let mut zip = ZipWriter::new(cursor); + zip.start_file("[Content_Types].xml", FileOptions::default())?; + zip.write_all(Self::content_types().as_bytes())?; + // add props_file to the folder buildTransitive\net46 + let props_file_name = format!("build/netstandard2.0/{}.props", package_id); + zip.start_file(&props_file_name, FileOptions::default())?; + zip.write_all(Self::props_file().as_bytes())?; + + // add targets_file to the folder buildTransitive\net46 + let targets_file_name = format!("build/netstandard2.0/{}.targets", package_id); + zip.start_file(&targets_file_name, FileOptions::default())?; + zip.write_all(Self::targets_file(package_id).as_bytes())?; + Ok(Self { zip }) + } + + fn content_types() -> String { + r#" + + + + + +"#.to_string() + } + + fn props_file() -> String { + r#" + + + + + PreserveNewest + false + %(Filename)%(Extension) + + + + PreserveNewest + false + %(Filename)%(Extension) + + + + PreserveNewest + false + %(Filename)%(Extension) + + + +"# + .to_string() + } + + fn targets_file(package_id: &str) -> String { + format!( + r#" + + + true + + + + + + + + +"#, + package_id + ) + } + + pub fn add_nuspec(&mut self, nuspec: &Nuspec) -> io::Result<()> { + let nuspec_file_name = format!("{}.nuspec", nuspec.metadata.id); + let nuspec_content = nuspec.to_xml(); + self.zip + .start_file(&nuspec_file_name, FileOptions::default())?; + self.zip.write_all(nuspec_content.as_bytes())?; + Ok(()) + } + + pub fn add_files(&mut self, project: &Project) -> io::Result<()> { + for platform_dir in &project.platform_directories { + if !(matches!(platform_dir.os, Os::Linux | Os::Macos | Os::Windows) + && matches!(platform_dir.cpu, Cpu::X86_64 | Cpu::Aarch64)) + { + continue; + } + for loadable_file in &platform_dir.loadable_files { + let os = match platform_dir.os { + Os::Windows => "win", + Os::Linux => "linux", + Os::Macos => "osx", + Os::Android => "android", + Os::Ios => "ios", + Os::IosSimulator => "ios-simulator", + }; + let cpu = match platform_dir.cpu { + Cpu::Aarch64 => "arm64", + Cpu::X86_64 => "x64", + Cpu::I686 => "x86", + Cpu::Armv7a => "armv7", + }; + let file_path = + format!("runtimes/{}-{}/native/{}", os, cpu, loadable_file.file.name); + self.zip.start_file(&file_path, FileOptions::default())?; + self.zip.write_all(&loadable_file.file.data)?; + } + } + Ok(()) + } + + pub fn add_cs_file(&mut self, project: &Project) -> io::Result<()> { + let package_name = project.spec.targets.nuget.as_ref().map_or_else( + || project.spec.package.name.replace('-', "_"), + |nuget| nuget.friendly_name.clone(), + ); + let cs_file_content = format!( + r#"// + +namespace Microsoft.Data.Sqlite +{{ + public static class Sqlite{0}Extensions + {{ + public static void Load{0}(this SqliteConnection connection) + => connection.LoadExtension("{1}"); + }} +}}"#, + package_name, + project.platform_directories[0].loadable_files[0] + .file + .name + .replace(".dll", "") + .replace(".so", "") + .replace(".dylib", "") + ); + let cs_file_path = format!( + "contentFiles/cs/netstandard2.0/{}/{}.cs", + package_name, package_name + ); + self.zip.start_file(&cs_file_path, FileOptions::default())?; + self.zip.write_all(cs_file_content.as_bytes())?; + Ok(()) + } + + fn update_nuspec_with_files(&self, nuspec: &mut Nuspec) { + nuspec.metadata.files = Some(vec![format!( + "", + nuspec.metadata.id + )]); + } + + pub fn finish(mut self) -> io::Result> { + let result = self.zip.finish()?; + Ok(result.into_inner()) + } +} + +pub(crate) fn write_nuget_packages( + project: &Project, + nuget_output_directory: &Path, +) -> io::Result> { + let mut assets = vec![]; + let mut nuspec = Nuspec::new(project); + let mut package = NugetPackage::new(&nuspec.metadata.id)?; + package.add_nuspec(&nuspec)?; + package.add_files(project)?; + package.add_cs_file(project)?; + package.update_nuspec_with_files(&mut nuspec); + let buffer = package.finish()?; + let output_path = nuget_output_directory.join(format!( + "{}.{}.nupkg", + nuspec.metadata.id, nuspec.metadata.version + )); + std::fs::write(&output_path, &buffer)?; + assets.push(GeneratedAsset::from( + GeneratedAssetKind::Nuget, + &output_path, + &buffer, + )?); + Ok(assets) +} diff --git a/src/spec.rs b/src/spec.rs index ffa39e6..81167d6 100644 --- a/src/spec.rs +++ b/src/spec.rs @@ -51,6 +51,11 @@ pub struct TargetAmalgamation { pub include: Vec, } +#[derive(Deserialize)] +pub struct TargetNuget { + pub friendly_name: String, +} + #[derive(Deserialize)] pub struct Targets { pub github_releases: Option, @@ -62,6 +67,7 @@ pub struct Targets { pub npm: Option, pub gem: Option, pub amalgamation: Option, + pub nuget: Option, } #[derive(Deserialize)] pub struct Spec {