Skip to content

Commit

Permalink
Cargo print (#9)
Browse files Browse the repository at this point in the history
  • Loading branch information
jzombie authored Feb 17, 2025
1 parent d5c4f4b commit b78f760
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 37 deletions.
18 changes: 17 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion cargo-pkg-info-struct-builder/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
[package]
name = "cargo-pkg-info-struct-builder"
version = "0.1.0-alpha7"
version = "0.1.0-alpha8"
authors = ["Jeremy Harris <jeremy.harris@zenosmosis.com>"]
edition = "2021"
description = "A Rust crate used as a build dependency which provides structured access to Cargo Package Info."
repository = "/~https://github.com/jzombie/rust-cargo-pkg-info-struct-builder"
license = "MIT"

[dependencies]
string-auto-indent = "0.1.0-alpha"
toml = "0.8.20"

[dev-dependencies]
Expand Down
97 changes: 82 additions & 15 deletions cargo-pkg-info-struct-builder/src/inject_build_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,9 @@ use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use string_auto_indent::auto_indent;
use toml::Value;

/// Escapes all newline sequences, normalizing `\r\n` to `\n`,
/// then replacing `\n` with `\\n`.
macro_rules! escape_newlines {
($s:expr) => {
$s.replace("\r\n", "\n").replace('\n', "\\n")
};
}

/// Injects build metadata, including license content if available.
///
/// This function gathers metadata such as:
Expand Down Expand Up @@ -53,24 +46,20 @@ pub fn inject_build_metadata(project_dest_path: PathBuf) {

// Retrieve the build target
let build_target = env::var("TARGET").unwrap_or_else(|_| "unknown-target".to_string());
println!("cargo:rustc-env=BUILD_TARGET={}", build_target);
set_cargo_env_var("BUILD_TARGET", &build_target);

// Get the build time in UTC
let build_time_utc = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("Time went backwards")
.as_secs()
.to_string();
println!("cargo:rustc-env=BUILD_TIME_UTC={}", build_time_utc);
set_cargo_env_var("BUILD_TIME_UTC", &build_time_utc);

// Read and set the license content if available
if let Some(license_path) = get_license_file_path(&manifest_dir) {
if let Ok(license_content) = fs::read_to_string(&license_path) {
// Set license content as an environment variable
println!(
"cargo:rustc-env=LICENSE_CONTENT={}",
escape_newlines!(license_content)
);
set_cargo_env_var("LICENSE_CONTENT", &license_content);
}
}

Expand Down Expand Up @@ -103,6 +92,12 @@ pub fn inject_build_metadata(project_dest_path: PathBuf) {
}
}

/// Escapes all newline sequences, normalizing `\r\n` to `\n`,
/// then replacing `\n` with `\\n`.
fn escape_newlines(s: &str) -> String {
s.replace("\r\n", "\n").replace('\n', "\\n")
}

/// Reads `Cargo.toml`, parses it, and extracts the value of a specified field.
///
/// This function reads the `[package]` section of `Cargo.toml` and returns the value
Expand Down Expand Up @@ -160,3 +155,75 @@ pub fn get_cargo_field(manifest_dir: &Path, field: &str) -> Option<String> {
pub fn get_license_file_path(manifest_dir: &Path) -> Option<PathBuf> {
get_cargo_field(manifest_dir, "license-file").map(|rel_path| manifest_dir.join(rel_path))
}

/// Sets an environment variable for Cargo at build time, ensuring the variable name is valid.
///
/// # Arguments
/// * `var_name` - The name of the environment variable (validated).
/// * `value` - The value to assign, with escaped newlines.
///
/// # Panics
/// This function will panic if:
/// - The variable name is empty.
/// - The variable name contains invalid characters.
/// - The variable name starts with a non-alphabetic character.
///
/// ```
/// use cargo_pkg_info_struct_builder::set_cargo_env_var;
/// set_cargo_env_var("BUILD_INFO", "Rust Build System");
/// ```
pub fn set_cargo_env_var(var_name: &str, value: &str) {
assert!(
is_valid_env_var_name(var_name),
"Invalid Cargo environment variable name: '{}'",
var_name
);

assert!(
!var_name.starts_with("CARGO_"),
"Environment variable name '{}' is reserved (cannot start with 'CARGO_')",
var_name
);

let formatted_value = escape_newlines(value); // Escape newlines
println!("cargo:rustc-env={}={}", var_name, formatted_value);
}

/// Sets an environment variable for Cargo with auto-indented multi-line content.
///
/// This function applies `auto_indent` to normalize indentation before
/// setting the environment variable, ensuring consistent formatting.
///
/// # Arguments
/// * `var_name` - The name of the environment variable.
/// * `value` - The multi-line string value to be auto-indented and assigned.
///
/// # Behavior
/// - Ensures proper indentation is maintained when storing multi-line values.
/// - Calls `set_cargo_env_var` after processing the string.
pub fn set_multi_line_cargo_env_var(var_name: &str, value: &str) {
let auto_indented_value = auto_indent(value);

set_cargo_env_var(var_name, auto_indented_value.as_str());
}

/// Validates if an environment variable name follows Cargo and POSIX conventions.
///
/// - Must start with an ASCII letter (`A-Z` or `a-z`).
/// - Can contain uppercase letters (`A-Z`), digits (`0-9`), and underscores (`_`).
/// - Cannot contain spaces, special characters, or start with a digit.
///
/// # Returns
/// `true` if the name is valid, `false` otherwise.
fn is_valid_env_var_name(name: &str) -> bool {
let mut chars = name.chars();

// First character must be a letter (A-Z or a-z)
match chars.next() {
Some(c) if c.is_ascii_alphabetic() => (),
_ => return false,
}

// Remaining characters must be A-Z, 0-9, or _
chars.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@

pub struct CargoPkgInfo {}

/// Unescapes all `\\n` sequences back to `\n`.
macro_rules! unescape_newlines {
($s:expr) => {
Box::leak($s.replace("\\n", "\n").into_boxed_str())
};
}

impl CargoPkgInfo {
/// Returns the package name.
#[allow(dead_code)]
Expand Down Expand Up @@ -120,7 +113,7 @@ impl CargoPkgInfo {
return None;
}

Some(unescape_newlines!(license.unwrap()))
Some(Self::unescape_newlines(license.unwrap()))
}

/// Returns the Rust version required by the package.
Expand All @@ -146,4 +139,9 @@ impl CargoPkgInfo {
pub fn build_time_utc() -> Option<u64> {
option_env!("BUILD_TIME_UTC").and_then(|s| s.parse::<u64>().ok())
}

/// Converts `\\n` sequences back to `\n`.
pub fn unescape_newlines(s: &str) -> &'static str {
Box::leak(s.replace("\\n", "\n").into_boxed_str())
}
}
2 changes: 1 addition & 1 deletion cargo-pkg-info-struct-builder/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
pub mod inject_build_metadata;
pub use inject_build_metadata::inject_build_metadata;
pub use inject_build_metadata::{inject_build_metadata, set_cargo_env_var};
15 changes: 15 additions & 0 deletions cargo-pkg-info-struct-builder/tests/test_inject_metadata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use cargo_pkg_info_struct_builder::inject_build_metadata;
use std::fs;
use syn;

// Note: Additional tests are performed directly in the `cargo-pkg-info-test-app` workspace.

#[test]
fn test_inject_build_metadata() {
let temp_dir = tempfile::tempdir().unwrap();
Expand Down Expand Up @@ -60,3 +62,16 @@ fn test_inject_build_metadata_no_mtime_change() {
let contents = fs::read_to_string(&dest_path).unwrap();
syn::parse_file(&contents).expect("Generated Rust file is invalid on second parse!");
}

/// Ensures that invalid environment variable names are rejected.
#[test]
#[should_panic(expected = "Invalid Cargo environment variable name")]
fn test_invalid_env_var_names() {
cargo_pkg_info_struct_builder::set_cargo_env_var("INVALID VAR", "test value");
}

#[test]
#[should_panic(expected = "Environment variable name 'CARGO_TEST_VAR' is reserved")]
fn test_reject_reserved_cargo_vars() {
cargo_pkg_info_struct_builder::set_cargo_env_var("CARGO_TEST_VAR", "test value");
}
5 changes: 3 additions & 2 deletions cargo-pkg-info-test-app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ readme = "README.md"
license-file = "MOCK-LICENSE.txt"
publish = false

[dependencies]

[build-dependencies]
cargo-pkg-info-struct-builder = { path = "../cargo-pkg-info-struct-builder" }

[dependencies]

17 changes: 16 additions & 1 deletion cargo-pkg-info-test-app/build.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
use cargo_pkg_info_struct_builder::inject_build_metadata;
use cargo_pkg_info_struct_builder::{
inject_build_metadata::{inject_build_metadata, set_multi_line_cargo_env_var},
set_cargo_env_var,
};
use std::path::Path;

fn main() {
set_cargo_env_var("TEST_CUSTOM_ENV_VAR", "TEST_RESULT");
set_multi_line_cargo_env_var(
"TEST_MULTI_LINE_CUSTOM_ENV_VAR",
r#"
Some multi-line environment variable
Level 1
Level 2
Level 3
"#,
);

inject_build_metadata(Path::new("src").join("cargo_pkg_info.rs").to_path_buf());
}
14 changes: 6 additions & 8 deletions cargo-pkg-info-test-app/src/cargo_pkg_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,6 @@

pub struct CargoPkgInfo {}

/// Unescapes all `\\n` sequences back to `\n`.
macro_rules! unescape_newlines {
($s:expr) => {
Box::leak($s.replace("\\n", "\n").into_boxed_str())
};
}

impl CargoPkgInfo {
/// Returns the package name.
#[allow(dead_code)]
Expand Down Expand Up @@ -120,7 +113,7 @@ impl CargoPkgInfo {
return None;
}

Some(unescape_newlines!(license.unwrap()))
Some(Self::unescape_newlines(license.unwrap()))
}

/// Returns the Rust version required by the package.
Expand All @@ -146,4 +139,9 @@ impl CargoPkgInfo {
pub fn build_time_utc() -> Option<u64> {
option_env!("BUILD_TIME_UTC").and_then(|s| s.parse::<u64>().ok())
}

/// Converts `\\n` sequences back to `\n`.
pub fn unescape_newlines(s: &str) -> &'static str {
Box::leak(s.replace("\\n", "\n").into_boxed_str())
}
}
14 changes: 14 additions & 0 deletions cargo-pkg-info-test-app/tests/test_pkg_info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,3 +160,17 @@ fn test_pkg_info() {
"Expected build time to be available, but it was None"
);
}

#[test]
fn test_custom_vars() {
// Refer to `build.rs` in `cargo-pkg-info-test-app` for the setting of these

assert_eq!(option_env!("TEST_CUSTOM_ENV_VAR"), Some("TEST_RESULT"));

assert_eq!(
CargoPkgInfo::unescape_newlines(option_env!("TEST_MULTI_LINE_CUSTOM_ENV_VAR").unwrap()),
CargoPkgInfo::unescape_newlines(
"Some multi-line environment variable\n\nLevel 1\n Level 2\n Level 3\n"
)
);
}

0 comments on commit b78f760

Please sign in to comment.