packc/cli/
extensions_lock.rs1#![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 #[arg(long = "in", value_name = "DIR", default_value = ".")]
22 pub input: PathBuf,
23
24 #[arg(long = "file", value_name = "FILE")]
26 pub file: Option<PathBuf>,
27
28 #[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}