Skip to main content

packc/cli/
extensions_lock.rs

1#![forbid(unsafe_code)]
2
3use std::future::Future;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Context, Result};
7use clap::Args;
8use greentic_distributor_client::{DistClient, DistOptions};
9use tokio::runtime::Handle;
10
11use crate::extension_refs::{
12    LockedExtensionDependency, PackExtensionsLockFile, default_extensions_file_path,
13    default_extensions_lock_file_path, pin_reference, read_extensions_file,
14    write_extensions_lock_file,
15};
16use crate::runtime::RuntimeContext;
17
18#[derive(Debug, Args)]
19pub struct ExtensionsLockArgs {
20    /// Pack root directory containing pack.yaml / pack.extensions.json.
21    #[arg(long = "in", value_name = "DIR", default_value = ".")]
22    pub input: PathBuf,
23
24    /// Override source file path (default: <pack_dir>/pack.extensions.json).
25    #[arg(long = "file", value_name = "FILE")]
26    pub file: Option<PathBuf>,
27
28    /// Override output path (default: <pack_dir>/pack.extensions.lock.json).
29    #[arg(long = "out", value_name = "FILE")]
30    pub out: Option<PathBuf>,
31}
32
33pub async fn handle(
34    args: ExtensionsLockArgs,
35    runtime: &RuntimeContext,
36    emit_path: bool,
37) -> Result<()> {
38    let pack_dir = args
39        .input
40        .canonicalize()
41        .with_context(|| format!("failed to resolve pack dir {}", args.input.display()))?;
42    let source_path = resolve_path(
43        &pack_dir,
44        args.file.as_deref(),
45        default_extensions_file_path(&pack_dir),
46    );
47    let out_path = resolve_path(
48        &pack_dir,
49        args.out.as_deref(),
50        default_extensions_lock_file_path(&pack_dir),
51    );
52
53    let source = read_extensions_file(&source_path)?;
54    let mut locked = Vec::with_capacity(source.extensions.len());
55    let handle = Handle::try_current().context("extension locking requires a Tokio runtime")?;
56
57    for extension in source.extensions {
58        let dist = DistClient::new(DistOptions {
59            cache_dir: runtime.cache_dir(),
60            allow_tags: extension.source.allow_tags,
61            offline: runtime.network_policy().is_offline(),
62            allow_insecure_local_http: false,
63            ..DistOptions::default()
64        });
65        let source = dist
66            .parse_source(&extension.source.reference)
67            .with_context(|| format!("parse extension ref {}", extension.source.reference))?;
68        let descriptor = block_on(
69            &handle,
70            dist.resolve(source, greentic_distributor_client::ResolvePolicy),
71        )
72        .with_context(|| format!("resolve extension ref {}", extension.source.reference))?;
73        let resolved = block_on(
74            &handle,
75            dist.fetch(&descriptor, greentic_distributor_client::CachePolicy),
76        )
77        .with_context(|| format!("fetch extension ref {}", extension.source.reference))?;
78        let digest = if resolved.resolved_digest.is_empty() {
79            resolved.digest.clone()
80        } else {
81            resolved.resolved_digest.clone()
82        };
83        let resolved_ref = pin_reference(&extension.source.reference, &digest);
84        locked.push(LockedExtensionDependency {
85            id: extension.id,
86            role: extension.role,
87            source_ref: extension.source.reference,
88            resolved_ref,
89            digest,
90            media_type: resolved.content_type.clone(),
91            size_bytes: resolved.content_length,
92        });
93    }
94
95    let lock = PackExtensionsLockFile::new(locked);
96    write_extensions_lock_file(&out_path, &lock)?;
97    if emit_path {
98        eprintln!(
99            "{}",
100            crate::cli_i18n::tf("cli.common.wrote_path", &[&out_path.display().to_string()])
101        );
102    }
103    Ok(())
104}
105
106fn resolve_path(pack_dir: &Path, override_path: Option<&Path>, default: PathBuf) -> PathBuf {
107    match override_path {
108        Some(path) if path.is_absolute() => path.to_path_buf(),
109        Some(path) => pack_dir.join(path),
110        None => default,
111    }
112}
113
114fn block_on<F, T, E>(handle: &Handle, fut: F) -> std::result::Result<T, E>
115where
116    F: Future<Output = std::result::Result<T, E>>,
117{
118    tokio::task::block_in_place(|| handle.block_on(fut))
119}
120
121trait OfflineCheck {
122    fn is_offline(&self) -> bool;
123}
124
125impl OfflineCheck for crate::runtime::NetworkPolicy {
126    fn is_offline(&self) -> bool {
127        matches!(self, crate::runtime::NetworkPolicy::Offline)
128    }
129}