forte-cli 0.3.37

CLI for the Forte fullstack web framework
use std::env;
use std::fs;
use std::os::unix::fs::symlink;
use std::path::{Path, PathBuf};

fn main() {
    let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".to_string());
    let manifest_dir = env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set");

    let target_dir = resolve_target_dir(Path::new(&manifest_dir));
    let target_binary = target_dir.join(&profile).join("forte");

    let home = env::var("HOME").expect("HOME not set");
    let cargo_bin = PathBuf::from(&home).join(".cargo").join("bin");

    if !cargo_bin.exists() {
        fs::create_dir_all(&cargo_bin).ok();
    }

    let symlink_path = cargo_bin.join("forte");

    if symlink_path.exists() || symlink_path.is_symlink() {
        fs::remove_file(&symlink_path).ok();
    }

    if let Err(e) = symlink(&target_binary, &symlink_path) {
        println!(
            "cargo:warning=Failed to create symlink: {} -> {}: {}",
            symlink_path.display(),
            target_binary.display(),
            e
        );
    } else {
        println!(
            "cargo:warning=Symlinked: {} -> {}",
            symlink_path.display(),
            target_binary.display()
        );
    }

    let workspace_root = resolve_workspace_root(Path::new(&manifest_dir))
        .expect("workspace root from forte/cli manifest dir");

    let targets: &[(&str, &str, &str)] = &[
        ("FORTE_JSON_VERSION", "forte/json/Cargo.toml", "forte-json"),
        ("FORTE_SDK_VERSION", "forte/sdk/Cargo.toml", "forte-sdk"),
        (
            "FORTE_CODEGEN_VERSION",
            "forte/codegen/Cargo.toml",
            "forte-codegen",
        ),
        ("FN0_DOC_DB_VERSION", "doc-db/Cargo.toml", "fn0-doc-db"),
        (
            "FN0_OBJECT_STORAGE_VERSION",
            "object-storage/Cargo.toml",
            "fn0-object-storage",
        ),
    ];

    for (env_var, rel_path, expected_name) in targets {
        let manifest = workspace_root.join(rel_path);
        let version = read_package_version(&manifest, expected_name);
        println!("cargo:rustc-env={env_var}={version}");
        println!("cargo:rerun-if-changed={}", manifest.display());
    }

    println!("cargo:rerun-if-env-changed=PROFILE");
    println!("cargo:rerun-if-env-changed=CARGO_TARGET_DIR");
}

fn resolve_target_dir(manifest_dir: &Path) -> PathBuf {
    if let Ok(t) = env::var("CARGO_TARGET_DIR") {
        return PathBuf::from(t);
    }
    resolve_workspace_root(manifest_dir)
        .map(|root| root.join("target"))
        .unwrap_or_else(|| manifest_dir.join("target"))
}

fn resolve_workspace_root(manifest_dir: &Path) -> Option<PathBuf> {
    let mut dir = manifest_dir.parent();
    while let Some(d) = dir {
        let manifest = d.join("Cargo.toml");
        if manifest.exists() {
            let content = fs::read_to_string(&manifest).unwrap_or_default();
            if content.contains("[workspace]") {
                return Some(d.to_path_buf());
            }
        }
        dir = d.parent();
    }
    None
}

fn read_package_version(path: &Path, expected_name: &str) -> String {
    let content = fs::read_to_string(path)
        .unwrap_or_else(|e| panic!("read {}: {e}", path.display()));

    let mut in_package = false;
    let mut found_name: Option<String> = None;
    let mut found_version: Option<String> = None;

    for line in content.lines() {
        let trimmed = line.trim();
        if let Some(section) = trimmed.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
            in_package = section == "package";
            continue;
        }
        if !in_package {
            continue;
        }
        if let Some(rest) = trimmed.strip_prefix("name") {
            found_name = Some(extract_string_value(rest));
        } else if let Some(rest) = trimmed.strip_prefix("version") {
            found_version = Some(extract_string_value(rest));
        }
        if found_name.is_some() && found_version.is_some() {
            break;
        }
    }

    let name = found_name.unwrap_or_else(|| {
        panic!("no [package].name in {}", path.display());
    });
    assert_eq!(
        name,
        expected_name,
        "{} has [package].name = {name:?}, expected {expected_name:?}",
        path.display()
    );

    found_version.unwrap_or_else(|| panic!("no [package].version in {}", path.display()))
}

fn extract_string_value(rest: &str) -> String {
    let after_eq = rest
        .trim_start()
        .strip_prefix('=')
        .expect("expected `=` after key")
        .trim();
    after_eq
        .trim_matches('"')
        .to_string()
}