pray-cli 1.20.0

Package manager for the language placed before inference — Prayfile CLI (binary: pray)
use crate::materialize::build_package_archive_bytes;
use crate::project_paths::manifest_path;
use crate::publish_dest::publish_destinations;
use crate::publish_integrity::{stored_package_artifact, stored_publish_matches};
use crate::publish_plan::{print_publish_plan, selected_packages};
use crate::publish_ssh::publish_to_ssh_server;
use crate::publish_version::published_registry_package_version;
use crate::registry_ops::{
    current_signer, current_signer_fingerprint, current_timestamp, load_registry_index,
    load_registry_package_metadata, registry_artifact_path, registry_metadata_path,
    torrent_manifest_bytes, torrent_manifest_path, write_output_bytes, write_registry_index,
    write_registry_package_metadata, write_torrent_manifest,
};
use crate::revision::{record_root_revision, RevisionAction};
use crate::sync_peers::map_transport_error;
use crate::transport_metadata::transport_package_metadata;
use pray_core::distribution::{
    fetch_registry_distribution_settings, read_registry_distribution_settings,
};
use pray_core::package_integrity::resolve_publish_signing_key;
use pray_core::publish_remote::PublishCliDest;
use pray_core::registry::{
    publish_authorization_header, upload_registry_artifact_with_authorization,
    RegistryPackageMetadata,
};
use pray_core::resolve::{resolve_project, ResolvedPackage};
use pray_core::{PrayError, PrayResult};
use pray_transport::{PeerConfig, SyncDirection, TransportRegistry, TrustLevel};
use std::path::{Path, PathBuf};

pub(crate) fn publish_command(
    roots: Vec<PathBuf>,
    servers: Vec<String>,
    to: Vec<String>,
    signing_key_path: Option<PathBuf>,
    resign: bool,
    dry_run: bool,
) -> PrayResult<()> {
    let project = resolve_project(&manifest_path())?;
    let (_project_root, dests) = publish_destinations(PublishCliDest { to, roots, servers })?;
    let signer = current_signer()?;
    let signer_fingerprint = current_signer_fingerprint();
    let published_at = current_timestamp()?;
    let signing_key = resolve_publish_signing_key(signing_key_path.as_deref())?;
    if resign && signing_key.is_none() {
        return Err(PrayError::Usage(
            "publish --resign requires a signing key".to_string(),
        ));
    }
    if resign && dests.iter().any(|dest| dest.server.is_some()) {
        return Err(PrayError::Usage(
            "publish --resign requires a local root destination".to_string(),
        ));
    }
    if resign && dry_run {
        return Err(PrayError::Usage(
            "publish --resign cannot be combined with --dry-run".to_string(),
        ));
    }
    if dry_run {
        print_publish_plan(&project, &dests)?;
        return Ok(());
    }
    let runtime = if dests.iter().any(|dest| dest.server.is_some()) {
        Some(
            tokio::runtime::Builder::new_current_thread()
                .enable_all()
                .build()
                .map_err(|error| {
                    PrayError::Unsupported(format!("failed to start publish runtime: {error}"))
                })?,
        )
    } else {
        None
    };

    for dest in dests {
        let packages = selected_packages(&project, &dest.packages)?;
        if let Some(root) = &dest.root {
            publish_to_root(
                &packages,
                &signer,
                signer_fingerprint.as_deref(),
                published_at,
                signing_key.as_ref(),
                resign,
                root,
            )?;
            record_root_revision(root, RevisionAction::Publish)?;
        }
        if let (Some(server_url), Some(runtime)) = (&dest.server, &runtime) {
            publish_to_server(
                &packages,
                &signer,
                signer_fingerprint.as_deref(),
                published_at,
                signing_key.as_ref(),
                server_url,
                runtime,
            )?;
        }
    }
    Ok(())
}

pub(crate) fn publish_to_root(
    packages: &[&ResolvedPackage],
    signer: &str,
    signer_fingerprint: Option<&str>,
    published_at: u64,
    signing_key: Option<&ed25519_dalek::SigningKey>,
    resign: bool,
    root: &Path,
) -> PrayResult<()> {
    let mut registry_index = load_registry_index(root)?;
    let distribution = read_registry_distribution_settings(root)?;
    let mut package_names = registry_index
        .packages
        .iter()
        .cloned()
        .collect::<std::collections::BTreeSet<_>>();

    for package in packages {
        let artifact_path =
            registry_artifact_path(&package.declaration.name, &package.spec.version);
        let metadata_path = registry_metadata_path(root, &package.declaration.name);
        let mut metadata =
            load_registry_package_metadata(&metadata_path, &package.declaration.name)?;
        let existing = metadata
            .versions
            .iter()
            .find(|entry| entry.version == package.spec.version);
        let stored_artifact = existing.and_then(|entry| {
            stored_package_artifact(root, &artifact_path, package, entry, &distribution)
        });
        if !resign
            && existing
                .zip(stored_artifact.as_deref())
                .is_some_and(|(entry, artifact_bytes)| {
                    stored_publish_matches(artifact_bytes, package, entry)
                })
        {
            package_names.insert(package.declaration.name.clone());
            write_registry_package_metadata(&metadata_path, &metadata)?;
            continue;
        }

        let archive_bytes = build_package_archive_bytes(package)?;
        write_output_bytes(&root.join(&artifact_path), &archive_bytes)?;
        write_torrent_manifest(root, package, &artifact_path, &archive_bytes, &distribution)?;
        let mut version_entry = published_registry_package_version(
            package,
            signer,
            signer_fingerprint,
            published_at,
            signing_key,
            &artifact_path,
            &archive_bytes,
        )?;
        if let Some(existing) = metadata
            .versions
            .iter()
            .find(|entry| entry.version == version_entry.version)
        {
            version_entry.yanked = existing.yanked;
            if stored_artifact.is_some() {
                version_entry.published_at = existing.published_at;
            }
        }
        metadata
            .versions
            .retain(|entry| entry.version != version_entry.version);
        metadata.versions.push(version_entry);
        write_registry_package_metadata(&metadata_path, &metadata)?;
        package_names.insert(package.declaration.name.clone());
    }

    registry_index.packages = package_names.into_iter().collect();
    write_registry_index(root, &registry_index)?;
    Ok(())
}

pub(crate) fn publish_to_server(
    packages: &[&ResolvedPackage],
    signer: &str,
    signer_fingerprint: Option<&str>,
    published_at: u64,
    signing_key: Option<&ed25519_dalek::SigningKey>,
    server_url: &str,
    runtime: &tokio::runtime::Runtime,
) -> PrayResult<()> {
    if pray_core::ssh_client::is_pray_ssh_url(server_url) {
        return publish_to_ssh_server(
            packages,
            signer,
            signer_fingerprint,
            published_at,
            signing_key,
            server_url,
        );
    }

    let authorization = publish_authorization_header();
    let mut headers = serde_json::Map::new();
    if let Some(authorization) = authorization.as_ref() {
        headers.insert(
            "Authorization".to_string(),
            serde_json::Value::String(authorization.clone()),
        );
    }
    let peer = PeerConfig {
        name: server_url.to_string(),
        transport: "http".to_string(),
        url: Some(server_url.to_string()),
        trust: TrustLevel::Full,
        direction: SyncDirection::Push,
        config: serde_json::json!({ "headers": headers }),
    };
    let registry = TransportRegistry::new();
    let transport = registry.create(&peer).map_err(map_transport_error)?;
    let distribution = fetch_registry_distribution_settings(server_url)?;

    for package in packages {
        let archive_bytes = build_package_archive_bytes(package)?;
        let artifact_path =
            registry_artifact_path(&package.declaration.name, &package.spec.version);
        upload_registry_artifact_with_authorization(
            server_url,
            &artifact_path,
            &archive_bytes,
            authorization.as_deref(),
        )?;
        if distribution.allows_torrent() {
            upload_registry_artifact_with_authorization(
                server_url,
                &torrent_manifest_path(&artifact_path),
                &torrent_manifest_bytes(
                    package,
                    &artifact_path,
                    &archive_bytes,
                    distribution.bootstrap_trackers.clone(),
                )?,
                authorization.as_deref(),
            )?;
        }

        let metadata = RegistryPackageMetadata {
            name: package.declaration.name.clone(),
            versions: vec![published_registry_package_version(
                package,
                signer,
                signer_fingerprint,
                published_at,
                signing_key,
                &artifact_path,
                &archive_bytes,
            )?],
        };
        let transport_metadata = transport_package_metadata(&metadata);
        runtime
            .block_on(transport.push_package(&peer, &transport_metadata))
            .map_err(map_transport_error)?;
    }

    Ok(())
}