Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 89 additions & 15 deletions keetanetwork-asn1/build.rs
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
//! ISO 20022 schema refresh is optional on RO/docs.rs.
//! Rust outputs stay under `OUT_DIR`.
//! `src/generated.rs` stays the committed `include!` stub.

use std::env;
use std::fs;
use std::path::Path;
use std::io::{self, ErrorKind};
use std::path::{Path, PathBuf};

use serde_json::Value;

use keetanetwork_utils::build::{compile_asn1_directory_with_full_config, Asn1CompileConfig};

fn main() {
println!("cargo:rerun-if-env-changed=DOCS_RS");
println!("cargo:rerun-if-changed=asn1");
println!("cargo:rerun-if-changed=oids.json");

// Get OUT_DIR for generated files (required by cargo for publishable crates)
let out_dir = env::var("OUT_DIR").expect("OUT_DIR must be set by cargo");
let generated_dir = Path::new(&out_dir).join("generated");
Expand All @@ -18,14 +27,23 @@
.to_str()
.expect("OUT_DIR path must be valid UTF-8");

// Generate OID schema tokens
generate_schema();
// Generate OIDs from JSON
let schema_content = render_iso20022_schema();
let compile_asn_dir = stage_asn1_inputs(&generated_dir, &schema_content);
let compile_asn_dir_str = compile_asn_dir
.to_str()
.expect("OUT_DIR path must be valid UTF-8");
refresh_source_iso20022_schema(&schema_content);
generate_oids_from_json(generated_dir_str);

// Use OUT_DIR includes pattern for cargo publish compatibility
let config = Asn1CompileConfig::new("asn1", generated_dir_str)
let generated_rs_path = Path::new(&out_dir).join("generated.rs");
let generated_rs_path_str = generated_rs_path
.to_str()
.expect("OUT_DIR path must be valid UTF-8");

let config = Asn1CompileConfig::new(compile_asn_dir_str, generated_dir_str)
.with_out_dir_includes(true)
.with_generated_rs_path(generated_rs_path_str)
.with_clippy_fixes(false)
.with_strip_prebuilt_methods(true)
.with_methods_to_strip("algorithm_identifier_definitions", vec!["new"])
.with_methods_to_strip("subject_public_key_info_definitions", vec!["new"])
Expand Down Expand Up @@ -109,9 +127,8 @@
}
}

fn generate_schema() {
fn render_iso20022_schema() -> String {
let oids = load_oids_json();
let dest_path = Path::new("asn1").join("iso20022.asn");
let mut schema_content = String::new();

// Add ASN.1 module header
Expand All @@ -137,19 +154,76 @@
// Add module footer
schema_content.push_str("END\n");

// Ensure the asn1 directory exists
if let Some(parent) = dest_path.parent() {
fs::create_dir_all(parent).expect("Failed to create asn1 directory");
ensure_single_newline_ending(&mut schema_content);
schema_content
}

/// Copy `asn1/*.asn` into `OUT_DIR` and overlay the rendered ISO 20022 schema.
/// Compile reads this writable `OUT_DIR` tree so rasn always sees a writable asn1 input set.
fn stage_asn1_inputs(generated_dir: &Path, iso20022_schema: &str) -> PathBuf {
let staged = generated_dir.join("asn1");
fs::create_dir_all(&staged).expect("OUT_DIR must be writable during build");

let source_dir = Path::new("asn1");
let entries = fs::read_dir(source_dir).expect("asn1 inputs must be readable");
for entry in entries {
let entry = entry.expect("asn1 entry must be readable");
let path = entry.path();
let Some(name) = path.file_name() else {
continue;
};
if path.extension().and_then(|ext| ext.to_str()) != Some("asn") {

Check warning on line 175 in keetanetwork-asn1/build.rs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Replace this closure with a reference to the `to_str` method.

See more on https://sonarcloud.io/project/issues?id=KeetaNetwork_node-rs&issues=AaDF4ppJysBF97yuLW85&open=AaDF4ppJysBF97yuLW85&pullRequest=49
continue;
}
if name == "iso20022.asn" {
continue;
}
fs::copy(&path, staged.join(name)).expect("OUT_DIR must be writable during build");
}

ensure_single_newline_ending(&mut schema_content);
fs::write(staged.join("iso20022.asn"), iso20022_schema).expect("OUT_DIR must be writable during build");
staged
}

fn docs_rs_build() -> bool {
env::var_os("DOCS_RS").is_some()
}

fn is_source_ro_error(err: &io::Error) -> bool {
matches!(err.kind(), ErrorKind::PermissionDenied | ErrorKind::ReadOnlyFilesystem)
}

// Only write when the generated schema actually differs.
/// Refresh crate-source `asn1/iso20022.asn` when that tree is writable.
/// On docs.rs or a read-only source error, keep the committed input and return.
fn refresh_source_iso20022_schema(schema_content: &str) {
if docs_rs_build() {
return;
}

let dest_path = Path::new("asn1").join("iso20022.asn");
let unchanged = fs::read_to_string(&dest_path)
.map(|existing| existing == schema_content)
.unwrap_or(false);
if !unchanged {
fs::write(&dest_path, schema_content).expect("Failed to write iso20022.asn");
if unchanged {
return;
}

if let Some(parent) = dest_path.parent() {
if let Err(e) = fs::create_dir_all(parent) {
if is_source_ro_error(&e) {
println!("cargo:warning=skipping iso20022.asn refresh: {e}");
return;
}
panic!("Failed to create asn1 directory: {e}");
}
}

match fs::write(&dest_path, schema_content) {
Ok(()) => {}
Err(e) if is_source_ro_error(&e) => {
println!("cargo:warning=skipping iso20022.asn refresh: {e}");
}
Err(e) => panic!("Failed to write iso20022.asn: {e}"),
}
}

Expand Down
3 changes: 2 additions & 1 deletion keetanetwork-asn1/src/generated.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
//! This module contains all the generated ASN.1 structures and re-exports them
//! for use throughout the library.
//!
//! This file is automatically generated by build.rs - do not edit manually.
//! This file is a committed `include!` stub.
//! Module bodies are generated under `OUT_DIR`.

#[allow(unused_imports, unused_variables, dead_code, non_camel_case_types, clippy::too_many_arguments)]
mod algorithm_identifier_definitions {
Expand Down
138 changes: 138 additions & 0 deletions keetanetwork-asn1/tests/docsrs_readonly.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
//! Docs.rs sets `DOCS_RS` and mounts crate source read-only.
//! This test stages a writable `OUT_DIR` build.
//! It asserts `cargo build -p keetanetwork-asn1` succeeds because ASN.1 staging
//! and generated Rust stay under `OUT_DIR`.

#![cfg(all(feature = "std", feature = "rasn", feature = "serde"))]

use std::fs;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::time::{SystemTime, UNIX_EPOCH};

const WORKSPACE_ROOT_FILES: &[&str] = &["Cargo.toml", "Cargo.lock", "rust-toolchain.toml", "rustfmt.toml"];

#[test]
fn docsrs_readonly_source_allows_asn1_build() {
let Ok(crate_root) = PathBuf::from(env!("CARGO_MANIFEST_DIR")).canonicalize() else {
panic!("CARGO_MANIFEST_DIR must canonicalize");
};
let Some(workspace) = crate_root.parent() else {
panic!("keetanetwork-asn1 must live in the workspace");
};

let scratch = Scratch::create();
stage_workspace(workspace, &crate_root, &scratch.path);

// Force the `fs::write` branch: unchanged content would skip the source write.
let schema = scratch.path.join("keetanetwork-asn1/asn1/iso20022.asn");
fs::write(&schema, "stale schema to force generate_schema write\n")
.expect("scratch schema must be writable before chmod");

chmod_a_minus_w(scratch.path.join("keetanetwork-asn1"));

let output = cargo_docsrs_build(&scratch.path);
let log = command_log(&output);
assert!(output.status.success(), "DOCS_RS=1 + read-only crate source must not panic in build.rs; got:\n{log}");
}

fn stage_workspace(workspace: &Path, crate_root: &Path, scratch: &Path) {
for name in WORKSPACE_ROOT_FILES {
fs::copy(workspace.join(name), scratch.join(name)).expect("workspace root file must copy");
}

let cargo_dir = scratch.join(".cargo");
fs::create_dir_all(&cargo_dir).expect("scratch .cargo must exist");
fs::copy(workspace.join(".cargo/config.toml"), cargo_dir.join("config.toml"))
.expect(".cargo/config.toml must copy");

copy_tree(crate_root, &scratch.join("keetanetwork-asn1"));
link_sibling_members(workspace, scratch);
}

fn link_sibling_members(workspace: &Path, scratch: &Path) {
let entries = fs::read_dir(workspace).expect("workspace must be readable");
for entry in entries {
let entry = entry.expect("workspace entry must be readable");
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
if !name.starts_with("keetanetwork-") || name == "keetanetwork-asn1" {
continue;
}
let src = entry.path();
if !src.is_dir() {
continue;
}
std::os::unix::fs::symlink(src, scratch.join(name)).expect("sibling member must symlink");
}
}

fn copy_tree(from: &Path, to: &Path) {
fs::create_dir_all(to).expect("destination directory must exist");
let entries = fs::read_dir(from).expect("source tree must be readable");
for entry in entries {
let entry = entry.expect("source entry must be readable");
let src = entry.path();
let dest = to.join(entry.file_name());
if src.is_dir() {
copy_tree(&src, &dest);
continue;
}
fs::copy(&src, &dest).expect("source file must copy");
}
}

fn cargo_docsrs_build(scratch: &Path) -> Output {
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_string());
Command::new(cargo)
.args(["build", "-p", "keetanetwork-asn1", "--manifest-path"])
.arg(scratch.join("Cargo.toml"))
.env("DOCS_RS", "1")
.env("CARGO_TARGET_DIR", scratch.join("target"))
.env_remove("CARGO_BUILD_TARGET")
.output()
.expect("cargo build must spawn")
}

fn chmod_a_minus_w(path: PathBuf) {
let status = Command::new("chmod")
.args(["-R", "a-w"])
.arg(&path)
.status()
.expect("chmod must spawn");
assert!(status.success(), "chmod a-w must succeed on {}", path.display());
}

fn command_log(output: &Output) -> String {
let stdout = String::from_utf8_lossy(&output.stdout);
let stderr = String::from_utf8_lossy(&output.stderr);
format!("status={:?}\nstdout:\n{stdout}\nstderr:\n{stderr}", output.status.code())
}

struct Scratch {
path: PathBuf,
}

impl Scratch {
fn create() -> Self {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let path = std::env::temp_dir().join(format!("keetanetwork-asn1-docsrs-ro-{}-{nanos}", std::process::id()));
fs::create_dir_all(&path).expect("scratch directory must exist");
Self { path }
}
}

impl Drop for Scratch {
fn drop(&mut self) {
let _ = Command::new("chmod")
.args(["-R", "u+w"])
.arg(&self.path)
.status();
let _ = fs::remove_dir_all(&self.path);
}
}
Loading