From 992e1dedd4b1b02617a57c36134cde6f1cb38dba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrius=20Puk=C5=A1ta?= Date: Fri, 3 Jul 2026 12:04:37 +0300 Subject: [PATCH] kpar: write pretty-printed `.project.json` and `.meta.json` to the kpar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Andrius Pukšta --- core/src/commands/build.rs | 7 ++--- core/src/commands/index/add.rs | 11 ++++---- core/src/commands/index/init.rs | 5 ++-- core/src/commands/index/mod.rs | 5 ---- core/src/commands/index/remove.rs | 10 +++---- core/src/commands/index/yank.rs | 8 ++---- core/src/utils.rs | 9 ++++++ sysand/tests/cli_build.rs | 46 ++++++++++++++++++++++++++++++- 8 files changed, 73 insertions(+), 28 deletions(-) diff --git a/core/src/commands/build.rs b/core/src/commands/build.rs index 19ffd9c42..641a03bb7 100644 --- a/core/src/commands/build.rs +++ b/core/src/commands/build.rs @@ -20,7 +20,7 @@ use crate::{ local_src::{LocalSrcError, LocalSrcProject}, utils::{FsIoError, ZipArchiveError, wrapfs}, }, - utils::{format_err, license_file_stems, sha256_lowercase_hex}, + utils::{format_err, license_file_stems, sha256_lowercase_hex, to_pretty_json_string}, workspace::{Workspace, WorkspaceReadError}, }; @@ -450,9 +450,8 @@ fn do_build_kpar_inner, Pr: ProjectRead>( // top level, exactly one file named .project.json and exactly one file // named .meta.json.” - let info_content = - serde_json::to_string(&info).expect("BUG: failed to serialize .project.json"); - let meta_content = serde_json::to_string(&meta).expect("BUG: failed to serialize .meta.json"); + let info_content = to_pretty_json_string(&info); + let meta_content = to_pretty_json_string(&meta); zip.start_file(".project.json", options) .map_err(|e| ZipArchiveError::Write(Utf8Path::new(".project.json").into(), e))?; diff --git a/core/src/commands/index/add.rs b/core/src/commands/index/add.rs index af7cda642..9f56c48f3 100644 --- a/core/src/commands/index/add.rs +++ b/core/src/commands/index/add.rs @@ -9,7 +9,7 @@ use thiserror::Error; use super::{ INDEX_FILE_NAME, INFO_FILE_NAME, JsonFileError, KPAR_FILE_NAME, META_FILE_NAME, - VERSIONS_FILE_NAME, open_json_file, overwrite_file, to_json_string, + VERSIONS_FILE_NAME, open_json_file, overwrite_file, }; use crate::{ @@ -26,6 +26,7 @@ use crate::{ utils::{FsIoError, wrapfs}, }, purl::{is_valid_unnormalized_name, is_valid_unnormalized_publisher, normalize_field}, + utils::to_pretty_json_string, }; #[derive(Error, Debug)] @@ -265,8 +266,8 @@ pub fn do_index_add, P: AsRef, R: AsRef>( [_, _, ..] => return Err(IndexAddError::DuplicateProject { iri: iri.into() }), }; - let info_str = to_json_string(&info); - let meta_str = to_json_string(&meta); + let info_str = to_pretty_json_string(&info); + let meta_str = to_pretty_json_string(&meta); wrapfs::create_dir_all(&project_path)?; @@ -337,8 +338,8 @@ pub fn do_index_add, P: AsRef, R: AsRef>( }, ); - let versions_str = to_json_string(&versions_value); - let index_str = to_json_string(&index_value); + let versions_str = to_pretty_json_string(&versions_value); + let index_str = to_pretty_json_string(&index_value); let adding = "Adding"; let header = crate::style::get_style_config().header; diff --git a/core/src/commands/index/init.rs b/core/src/commands/index/init.rs index c96f64330..af0a18d60 100644 --- a/core/src/commands/index/init.rs +++ b/core/src/commands/index/init.rs @@ -9,11 +9,12 @@ use std::{ use camino::Utf8Path; use thiserror::Error; -use super::{INDEX_FILE_NAME, to_json_string}; +use super::INDEX_FILE_NAME; use crate::{ index::model::IndexJson, project::utils::{FsIoError, wrapfs}, + utils::to_pretty_json_string, }; #[derive(Error, Debug)] @@ -29,7 +30,7 @@ pub fn do_index_init>(index_root: R) -> Result<(), IndexInitE let header = crate::style::get_style_config().header; log::info!("{header}{creating:>12}{header:#} index"); let index = IndexJson { projects: vec![] }; - let index_str = to_json_string(&index); + let index_str = to_pretty_json_string(&index); wrapfs::create_dir_all(index_root.as_ref())?; let index_path = index_root.as_ref().join(INDEX_FILE_NAME); let mut file = fs::File::create_new(&index_path).map_err(|e| match e.kind() { diff --git a/core/src/commands/index/mod.rs b/core/src/commands/index/mod.rs index e2fc5e60f..b3f7e7d28 100644 --- a/core/src/commands/index/mod.rs +++ b/core/src/commands/index/mod.rs @@ -75,11 +75,6 @@ pub(crate) fn open_json_file( Ok((file, value)) } -pub(crate) fn to_json_string(value: &T) -> String { - // If this fails, it's a bug - serde_json::to_string_pretty(value).unwrap() -} - pub(crate) fn overwrite_file( file: &mut File, path: &Utf8Path, diff --git a/core/src/commands/index/remove.rs b/core/src/commands/index/remove.rs index 64a7faf65..aac9308dc 100644 --- a/core/src/commands/index/remove.rs +++ b/core/src/commands/index/remove.rs @@ -6,10 +6,7 @@ use std::fs::File; use camino::{Utf8Path, Utf8PathBuf}; use thiserror::Error; -use super::{ - INDEX_FILE_NAME, JsonFileError, VERSIONS_FILE_NAME, open_json_file, overwrite_file, - to_json_string, -}; +use super::{INDEX_FILE_NAME, JsonFileError, VERSIONS_FILE_NAME, open_json_file, overwrite_file}; use crate::{ index::{ @@ -17,6 +14,7 @@ use crate::{ model::{IndexJson, ProjectStatus, VersionEntry, VersionStatus, VersionsJson}, }, project::utils::{FsIoError, wrapfs}, + utils::to_pretty_json_string, }; #[derive(Debug, Error)] @@ -83,7 +81,7 @@ pub fn do_index_remove, R: AsRef>( } else { project_entry.status = ProjectStatus::Removed; } - let index_str = to_json_string(&index_value); + let index_str = to_pretty_json_string(&index_value); let project_path = index_root.join(parsed_iri.get_path()); let versions_path = project_path.join(VERSIONS_FILE_NAME); @@ -168,7 +166,7 @@ fn remove_versions bool>( let version_path = project_path.join(&version_entry.version); version_entry.status = VersionStatus::Removed; - let versions_str = to_json_string(&versions_value); + let versions_str = to_pretty_json_string(&versions_value); overwrite_file(versions_file, versions_path, &versions_str)?; wrapfs::remove_dir_all(version_path)?; } diff --git a/core/src/commands/index/yank.rs b/core/src/commands/index/yank.rs index b083baa66..7b08a08fa 100644 --- a/core/src/commands/index/yank.rs +++ b/core/src/commands/index/yank.rs @@ -4,10 +4,7 @@ use camino::{Utf8Path, Utf8PathBuf}; use thiserror::Error; -use super::{ - INDEX_FILE_NAME, JsonFileError, VERSIONS_FILE_NAME, open_json_file, overwrite_file, - to_json_string, -}; +use super::{INDEX_FILE_NAME, JsonFileError, VERSIONS_FILE_NAME, open_json_file, overwrite_file}; use crate::{ index::{ @@ -15,6 +12,7 @@ use crate::{ model::{IndexJson, VersionStatus, VersionsJson}, }, project::utils::{FsIoError, wrapfs}, + utils::to_pretty_json_string, }; #[derive(Debug, Error)] @@ -100,7 +98,7 @@ pub fn do_index_yank, I: AsRef, V: AsRef>( match version_entry.status { VersionStatus::Available => { version_entry.status = VersionStatus::Yanked; - let versions_str = to_json_string(&versions_value); + let versions_str = to_pretty_json_string(&versions_value); overwrite_file(&mut versions_file, &versions_path, &versions_str)?; } VersionStatus::Yanked => { diff --git a/core/src/utils.rs b/core/src/utils.rs index 267f83024..6f9444329 100644 --- a/core/src/utils.rs +++ b/core/src/utils.rs @@ -5,6 +5,7 @@ use std::{error::Error, fmt::Write as _}; use digest::{array::Array, typenum}; use indexmap::IndexSet; +use serde::Serialize; use sha2::{Digest, Sha256}; use thiserror::Error; use typed_path::{Utf8UnixPath, Utf8WindowsPath}; @@ -231,3 +232,11 @@ pub fn parse_relative_unix_path( Ok(Utf8UnixPath::new(path)) } + +pub fn to_pretty_json_string(value: &T) -> String { + // If this fails, it's a bug + let mut serialized = serde_json::to_string_pretty(value).unwrap(); + // Text files should have a trailing newline + serialized.push('\n'); + serialized +} diff --git a/sysand/tests/cli_build.rs b/sysand/tests/cli_build.rs index 069627ab1..29180d544 100644 --- a/sysand/tests/cli_build.rs +++ b/sysand/tests/cli_build.rs @@ -118,6 +118,45 @@ fn project_build() -> Result<(), Box> { Ok(()) } +/// `.project.json` and `.meta.json` are pretty-printed (multi-line, indented) +/// inside the built kpar, not minified onto a single line. +#[test] +fn project_build_pretty_prints_project_and_meta_json() -> Result<(), Box> { + let (_temp_dir, cwd, out) = run_sysand( + ["init", "--version", "1.2.3", "--name", "test_pretty"], + None, + )?; + + std::fs::write(cwd.join("test.sysml"), b"package P;\n")?; + + out.assert().success(); + + let out = run_sysand_in(&cwd, ["include", "--no-index-symbols", "test.sysml"], None)?; + out.assert().success(); + + let out = run_sysand_in(&cwd, ["build", "./test_build.kpar"], None)?; + out.assert().success(); + + for archive_path in [".project.json", ".meta.json"] { + let content = read_kpar_file(&cwd.join("test_build.kpar"), archive_path); + + assert!( + content.lines().count() > 1, + "{archive_path} should be pretty-printed across multiple lines, got: {content}" + ); + + let value: serde_json::Value = serde_json::from_str(&content)?; + let mut expected = serde_json::to_string_pretty(&value)?; + expected.push('\n'); + assert_eq!( + content, expected, + "{archive_path} should use standard pretty-printed formatting" + ); + } + + Ok(()) +} + #[test] fn build_errors_when_index_symbol_is_missing_from_file() -> Result<(), Box> { let (_temp_dir, cwd, out) = run_sysand( @@ -1190,7 +1229,7 @@ fn project_build_without_license() -> Result<(), Box> { Ok(()) } -fn assert_kpar_file(kpar_path: &camino::Utf8Path, archive_path: &str, expected: &str) { +fn read_kpar_file(kpar_path: &camino::Utf8Path, archive_path: &str) -> String { let file = std::fs::File::open(kpar_path).unwrap(); let mut archive = zip::ZipArchive::new(file).unwrap(); let mut entry = archive @@ -1198,6 +1237,11 @@ fn assert_kpar_file(kpar_path: &camino::Utf8Path, archive_path: &str, expected: .unwrap_or_else(|_| panic!("expected {archive_path} in {kpar_path}")); let mut content = String::new(); entry.read_to_string(&mut content).unwrap(); + content +} + +fn assert_kpar_file(kpar_path: &camino::Utf8Path, archive_path: &str, expected: &str) { + let content = read_kpar_file(kpar_path, archive_path); assert_eq!(content, expected, "{archive_path} mismatch in {kpar_path}"); }