app

Local-first trade for farms and co-ops
git clone https://radroots.dev/git/app.git
Log | Files | Refs | README | LICENSE

build.rs (10299B)


      1 use std::collections::{BTreeMap, BTreeSet};
      2 use std::fs;
      3 use std::path::{Path, PathBuf};
      4 
      5 use harvestcircle_product::{parser as product_manifest, provenance as source_provenance};
      6 use quote::ToTokens;
      7 use sha2::{Digest, Sha256};
      8 use syn::{ImplItem, Item, Visibility};
      9 
     10 const CONTRACT_SOURCES: &[&str] = &[
     11     "src/commands.rs",
     12     "src/contract.rs",
     13     "src/dto.rs",
     14     "src/lib.rs",
     15     "src/observer.rs",
     16 ];
     17 const BASELINE_PATH: &str = "../../compatibility/harvestcircle-ffi-v4.properties";
     18 const PRODUCT_MANIFEST_PATH: &str = "../../../config/product/harvestcircle-v1.properties";
     19 const SOURCE_PROVENANCE_PATH: &str = "../../provenance/harvestcircle-v1.toml";
     20 const STORAGE_CONTRACT_PATH: &str = "../harvestcircle_storage/src/contract.rs";
     21 const BASELINE_KEYS: &[&str] = &[
     22     "schema",
     23     "contract.id",
     24     "contract.major",
     25     "contract.minor",
     26     "contract.hash",
     27     "product.coordinate_digest",
     28     "snapshot.schema",
     29     "storage.schema.minimum",
     30     "storage.schema.current",
     31     "product.version",
     32     "package.version",
     33     "source.provenance_digest",
     34     "source.foundation_baseline",
     35 ];
     36 
     37 fn main() {
     38     for name in [
     39         "HARVESTCIRCLE_BUILD_SOURCE_COMMIT",
     40         "HARVESTCIRCLE_BUILD_SOURCE_DIRTY",
     41         "HARVESTCIRCLE_BUILD_RADROOTS_REVISION",
     42         "HARVESTCIRCLE_BUILD_RUST_TOOLCHAIN",
     43         "HARVESTCIRCLE_BUILD_JAVA_TOOLCHAIN",
     44         "HARVESTCIRCLE_BUILD_KOTLIN_TOOLCHAIN",
     45         "SOURCE_DATE_EPOCH",
     46     ] {
     47         println!("cargo:rerun-if-env-changed={name}");
     48     }
     49     for source in CONTRACT_SOURCES {
     50         println!("cargo:rerun-if-changed={source}");
     51     }
     52     println!("cargo:rerun-if-changed={STORAGE_CONTRACT_PATH}");
     53     println!("cargo:rerun-if-changed={BASELINE_PATH}");
     54     println!("cargo:rerun-if-changed={PRODUCT_MANIFEST_PATH}");
     55     println!("cargo:rerun-if-changed={SOURCE_PROVENANCE_PATH}");
     56 
     57     let baseline = parse_baseline(
     58         &fs::read_to_string(BASELINE_PATH).expect("read HarvestCircle FFI baseline"),
     59     );
     60     validate_baseline_inputs(&baseline);
     61 
     62     let mut metadata = Vec::new();
     63     for source in CONTRACT_SOURCES {
     64         collect_public_metadata(Path::new(source), &mut metadata);
     65     }
     66     metadata.push(format!(
     67         "storage-contract:{}",
     68         hex_digest(&fs::read(STORAGE_CONTRACT_PATH).expect("read storage contract"))
     69     ));
     70     metadata.sort();
     71     metadata.dedup();
     72     let normalized = metadata.join("\n");
     73     let contract_digest = hex_digest(normalized.as_bytes());
     74     assert_eq!(
     75         required(&baseline, "contract.hash"),
     76         contract_digest,
     77         "HarvestCircle FFI baseline hash is stale"
     78     );
     79     emit(
     80         "HARVESTCIRCLE_FFI_CONTRACT_ID",
     81         required(&baseline, "contract.id"),
     82     );
     83     emit(
     84         "HARVESTCIRCLE_PRODUCT_VERSION",
     85         required(&baseline, "product.version"),
     86     );
     87     emit(
     88         "HARVESTCIRCLE_PACKAGE_VERSION",
     89         required(&baseline, "package.version"),
     90     );
     91     emit(
     92         "HARVESTCIRCLE_PRODUCT_COORDINATE_DIGEST",
     93         required(&baseline, "product.coordinate_digest"),
     94     );
     95     emit(
     96         "HARVESTCIRCLE_SOURCE_PROVENANCE_DIGEST",
     97         required(&baseline, "source.provenance_digest"),
     98     );
     99     emit(
    100         "HARVESTCIRCLE_SOURCE_FOUNDATION_BASELINE",
    101         required(&baseline, "source.foundation_baseline"),
    102     );
    103     emit("HARVESTCIRCLE_FFI_CONTRACT_DIGEST", &contract_digest);
    104     let provenance_source =
    105         fs::read_to_string(SOURCE_PROVENANCE_PATH).expect("read source provenance as UTF-8");
    106     let provenance =
    107         source_provenance::parse(&provenance_source).expect("canonicalize source provenance");
    108     let mut build_provenance = Vec::new();
    109     for (output, input, default) in [
    110         (
    111             "HARVESTCIRCLE_BUILD_SOURCE_COMMIT",
    112             "HARVESTCIRCLE_BUILD_SOURCE_COMMIT",
    113             provenance.foundation_baseline(),
    114         ),
    115         (
    116             "HARVESTCIRCLE_BUILD_SOURCE_DIRTY",
    117             "HARVESTCIRCLE_BUILD_SOURCE_DIRTY",
    118             "unknown",
    119         ),
    120         (
    121             "HARVESTCIRCLE_BUILD_RADROOTS_REVISION",
    122             "HARVESTCIRCLE_BUILD_RADROOTS_REVISION",
    123             provenance.canonical_radroots_revision(),
    124         ),
    125         (
    126             "HARVESTCIRCLE_BUILD_RUST_TOOLCHAIN",
    127             "HARVESTCIRCLE_BUILD_RUST_TOOLCHAIN",
    128             "1.97.1",
    129         ),
    130         (
    131             "HARVESTCIRCLE_BUILD_JAVA_TOOLCHAIN",
    132             "HARVESTCIRCLE_BUILD_JAVA_TOOLCHAIN",
    133             "unknown",
    134         ),
    135         (
    136             "HARVESTCIRCLE_BUILD_KOTLIN_TOOLCHAIN",
    137             "HARVESTCIRCLE_BUILD_KOTLIN_TOOLCHAIN",
    138             "unknown",
    139         ),
    140         (
    141             "HARVESTCIRCLE_BUILD_SOURCE_DATE_EPOCH",
    142             "SOURCE_DATE_EPOCH",
    143             "0",
    144         ),
    145     ] {
    146         let value = std::env::var(input).unwrap_or_else(|_| default.to_owned());
    147         assert_build_value(input, &value);
    148         emit(output, &value);
    149         build_provenance.push(format!("{input}={value}"));
    150     }
    151     emit(
    152         "HARVESTCIRCLE_BUILD_PROVENANCE_DIGEST",
    153         &hex_digest(build_provenance.join("\n").as_bytes()),
    154     );
    155     fs::write(
    156         PathBuf::from(std::env::var_os("OUT_DIR").expect("OUT_DIR"))
    157             .join("ffi_contract_metadata.txt"),
    158         normalized,
    159     )
    160     .expect("write normalized FFI metadata");
    161 }
    162 
    163 fn assert_build_value(name: &str, value: &str) {
    164     assert!(!value.is_empty() && value.len() <= 128, "invalid {name}");
    165     assert!(
    166         value.bytes().all(|byte| byte.is_ascii_graphic()),
    167         "invalid {name}"
    168     );
    169     if name == "HARVESTCIRCLE_BUILD_SOURCE_DIRTY" {
    170         assert!(
    171             matches!(value, "true" | "false" | "unknown"),
    172             "invalid {name}"
    173         );
    174     }
    175     if name == "SOURCE_DATE_EPOCH" {
    176         let _: u64 = value
    177             .parse()
    178             .expect("SOURCE_DATE_EPOCH must be an unsigned integer");
    179     }
    180 }
    181 
    182 fn parse_baseline(source: &str) -> BTreeMap<String, String> {
    183     let mut values = BTreeMap::new();
    184     for (index, raw_line) in source.lines().enumerate() {
    185         let line = raw_line.trim();
    186         if line.is_empty() || line.starts_with('#') {
    187             continue;
    188         }
    189         let (key, value) = line
    190             .split_once('=')
    191             .unwrap_or_else(|| panic!("invalid FFI baseline line {}", index + 1));
    192         let key = key.trim();
    193         let value = value.trim();
    194         assert!(
    195             !key.is_empty() && !value.is_empty(),
    196             "empty FFI baseline entry"
    197         );
    198         assert!(
    199             values.insert(key.to_owned(), value.to_owned()).is_none(),
    200             "duplicate FFI baseline key: {key}"
    201         );
    202     }
    203     let actual = values.keys().map(String::as_str).collect::<BTreeSet<_>>();
    204     let expected = BASELINE_KEYS.iter().copied().collect::<BTreeSet<_>>();
    205     assert_eq!(
    206         actual, expected,
    207         "FFI baseline keys differ from the contract"
    208     );
    209     values
    210 }
    211 
    212 fn validate_baseline_inputs(baseline: &BTreeMap<String, String>) {
    213     assert_eq!(required(baseline, "schema"), "harvestcircle.ffi.v4");
    214     assert_eq!(
    215         required(baseline, "contract.id"),
    216         "harvestcircle-desktop-ffi-v4"
    217     );
    218     assert_eq!(required(baseline, "contract.major"), "4");
    219     assert_eq!(required(baseline, "contract.minor"), "5");
    220     assert_eq!(required(baseline, "snapshot.schema"), "1");
    221     assert_eq!(required(baseline, "storage.schema.minimum"), "1");
    222     assert_eq!(required(baseline, "storage.schema.current"), "3");
    223     assert_eq!(
    224         required(baseline, "product.version"),
    225         env!("CARGO_PKG_VERSION")
    226     );
    227 
    228     let product_source =
    229         fs::read_to_string(PRODUCT_MANIFEST_PATH).expect("read product manifest as UTF-8");
    230     let product_digest =
    231         product_manifest::digest(&product_source).expect("canonicalize product manifest");
    232     assert_eq!(
    233         required(baseline, "product.coordinate_digest"),
    234         product_digest
    235     );
    236     let provenance_source =
    237         fs::read_to_string(SOURCE_PROVENANCE_PATH).expect("read source provenance as UTF-8");
    238     let provenance =
    239         source_provenance::parse(&provenance_source).expect("canonicalize source provenance");
    240     assert_eq!(
    241         required(baseline, "source.provenance_digest"),
    242         provenance.digest()
    243     );
    244     assert_eq!(
    245         provenance.foundation_baseline(),
    246         required(baseline, "source.foundation_baseline")
    247     );
    248 
    249     assert_eq!(required(baseline, "storage.schema.current"), "3");
    250 }
    251 
    252 fn required<'a>(baseline: &'a BTreeMap<String, String>, key: &str) -> &'a str {
    253     baseline
    254         .get(key)
    255         .unwrap_or_else(|| panic!("missing FFI baseline key: {key}"))
    256         .as_str()
    257 }
    258 
    259 fn emit(name: &str, value: &str) {
    260     println!("cargo:rustc-env={name}={value}");
    261 }
    262 
    263 fn collect_public_metadata(path: &Path, output: &mut Vec<String>) {
    264     let source = fs::read_to_string(path).expect("read FFI source");
    265     let file = syn::parse_file(&source).expect("parse FFI source");
    266     for item in file.items {
    267         match item {
    268             Item::Const(item) if is_public(&item.vis) => push_tokens("const", item, output),
    269             Item::Enum(item) if is_public(&item.vis) => push_tokens("enum", item, output),
    270             Item::Fn(item) if is_public(&item.vis) => push_tokens("fn", item.sig, output),
    271             Item::Struct(item) if is_public(&item.vis) => push_tokens("struct", item, output),
    272             Item::Trait(item) if is_public(&item.vis) => push_tokens("trait", item, output),
    273             Item::Impl(item) => {
    274                 let owner = item.self_ty.to_token_stream().to_string();
    275                 for member in item.items {
    276                     if let ImplItem::Fn(function) = member
    277                         && is_public(&function.vis)
    278                     {
    279                         output.push(format!("method:{owner}:{}", function.sig.to_token_stream()));
    280                     }
    281                 }
    282             }
    283             _ => {}
    284         }
    285     }
    286 }
    287 
    288 fn push_tokens(kind: &str, value: impl ToTokens, output: &mut Vec<String>) {
    289     output.push(format!("{kind}:{}", value.to_token_stream()));
    290 }
    291 
    292 const fn is_public(visibility: &Visibility) -> bool {
    293     matches!(visibility, Visibility::Public(_))
    294 }
    295 
    296 fn hex_digest(bytes: &[u8]) -> String {
    297     Sha256::digest(bytes)
    298         .iter()
    299         .map(|byte| format!("{byte:02x}"))
    300         .collect()
    301 }