1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
pub mod bin_resolver;
pub mod builder;
pub(crate) mod cache;
pub mod cargo;
pub mod cli;
pub mod config;
pub mod crate_resolver;
pub mod cratespec;
pub mod downloader;
pub mod error;
pub mod git;
pub(crate) mod helpers;
pub mod http;
pub(crate) mod logging;
pub mod messages;
pub(crate) mod registry;
pub mod runner;
pub(crate) mod sbom;
#[cfg(test)]
pub(crate) mod testdata;
use std::sync::Arc;
use bin_resolver::BinaryResolver;
use builder::{BuildOptions, BuildOverrides, CrateBuilder};
use cache::Cache;
// Re-export this third-party crate type that is nonetheless part of this crate's public API
pub use cargo_metadata::Target;
use config::Config;
use crate_resolver::CrateResolver;
use cratespec::{CrateRequest, CrateSpec};
use downloader::CrateDownloader;
use error::Result;
use http::HttpClient;
/// Instance of the engine that powers the `cgx` tool.
///
/// This is packaged this way so that our `main.rs` is as minimal as possible. That's useful for a
/// few reasons, but in our particular case it's because we want to be able to add `cgx` as a crate
/// in others' workspaces so that it can be invoked with `cargo run` or aliases and always
/// available to everyone using the project whether or not they previously installed `cgx` on their
/// systems.
pub struct Cgx {
config: Config,
resolver: Arc<dyn CrateResolver>,
bin_resolver: Arc<dyn BinaryResolver>,
downloader: Arc<dyn CrateDownloader>,
builder: Arc<dyn CrateBuilder>,
reporter: messages::MessageReporter,
}
impl Cgx {
/// Create a new instance from a loaded configuration.
///
/// The config should be loaded using [`Config::load()`] with the CLI args.
pub fn new(config: Config, reporter: messages::MessageReporter) -> Result<Self> {
tracing::debug!("Using config: {:#?}", config);
let http_client = HttpClient::new(&config.http)?;
let cache = Cache::new(config.clone(), reporter.clone());
let git_client = git::GitClient::new(cache.clone(), reporter.clone(), config.http.clone());
let cargo_runner = Arc::new(cargo::create_cargo_runner(config.clone(), reporter.clone())?);
let resolver = Arc::new(crate_resolver::create_resolver(
config.clone(),
cache.clone(),
git_client.clone(),
cargo_runner.clone(),
http_client.clone(),
));
let bin_resolver = Arc::new(bin_resolver::create_resolver(
config.clone(),
cache.clone(),
reporter.clone(),
http_client.clone(),
));
let downloader = Arc::new(downloader::create_downloader(
config.clone(),
cache.clone(),
git_client,
http_client,
));
let builder = Arc::new(builder::create_builder(config.clone(), cache, cargo_runner));
Ok(Self {
config,
resolver,
bin_resolver,
downloader,
builder,
reporter,
})
}
/// Resolve a crate request into its build plan and produce the path to its binary.
///
/// This is the shared load-and-build impl that powers `cgx <crate>`, `--no-exec`, `--prefetch`,
/// and each crate prefetched by `--prefetch-all`:
/// - loads the [`CrateSpec`] and [`BuildOptions`] from the engine's config plus `overrides`,
/// - resolves the spec to a concrete version and downloads the source,
/// - returns a pre-built binary if one is available and enabled, otherwise builds from source.
///
/// Returns the fully-qualified path to the crate's binary. Does NOT execute it.
pub fn prepare_bin_crate(
&self,
request: &CrateRequest,
overrides: &BuildOverrides,
) -> Result<std::path::PathBuf> {
let (crate_spec, build_options) = self.resolve_crate_request(request, overrides)?;
let downloaded_crate = self.resolve_and_download_crate_spec(&crate_spec, &build_options)?;
// Try to resolve a pre-built binary, now with access to the downloaded source
tracing::debug!("Attempting to resolve pre-built binary");
if let Some(resolved_binary) = self.bin_resolver.resolve(&downloaded_crate, &build_options)? {
let provider = resolved_binary.provider;
tracing::info!(
"Found pre-built binary from {:?} at: {}",
provider,
resolved_binary.path.display()
);
self.reporter.report(|| {
messages::CgxMessage::crate_provenance_prebuilt(
&downloaded_crate.resolved,
&downloaded_crate.crate_path,
&build_options,
provider,
&resolved_binary.path,
)
});
return Ok(resolved_binary.path);
}
// No pre-built binary available, fall back to building from source
tracing::info!(
"Pre-built binary not found, excluded by config, or disabled; building crate from source..."
);
let (bin_path, target_binary) = self.builder.build(&downloaded_crate, &build_options)?;
tracing::info!("Built crate binary at: {}", bin_path.display());
self.reporter.report(|| {
messages::CgxMessage::crate_provenance_built_from_source(
&downloaded_crate.resolved,
&downloaded_crate.crate_path,
&build_options,
&bin_path,
target_binary,
)
});
Ok(bin_path)
}
/// Resolve a crate request and list its runnable targets without building or executing.
///
/// Loads the [`CrateSpec`]/[`BuildOptions`] from config plus `overrides` and downloads the
/// source if needed, but does not build from source or look for a pre-built binary.
///
/// Returns `(crate_name, default_target, bin_targets, example_targets)`.
#[expect(
clippy::type_complexity,
reason = "the returned 4-tuple is documented above and clearer here than a one-off named struct"
)]
pub fn list_targets(
&self,
request: &CrateRequest,
overrides: &BuildOverrides,
) -> Result<(String, Option<Target>, Vec<Target>, Vec<Target>)> {
let (crate_spec, build_options) = self.resolve_crate_request(request, overrides)?;
let downloaded_crate = self.resolve_and_download_crate_spec(&crate_spec, &build_options)?;
let crate_name = downloaded_crate.resolved.name.clone();
let (default, bins, examples) = self.builder.list_targets(&downloaded_crate, &build_options)?;
Ok((crate_name, default, bins, examples))
}
/// Enumerate the configured tools and aliases in the config, then return the rendered
/// `[tools]`/`[aliases]` TOML.
pub fn list_configured_tools(&self) -> Result<String> {
// Emit appropriate messages as we enumerate the tools/aliases
for (name, tool_config) in self.config.sorted_tools() {
self.reporter
.report(|| messages::RunnerMessage::list_tool(name, tool_config));
}
for (name, target) in self.config.sorted_aliases() {
self.reporter
.report(|| messages::RunnerMessage::list_alias(name, target));
}
self.config.tools_toml()
}
/// Prefetch a single crate request: prepare its binary without executing it, reporting
/// progress via [`messages::RunnerMessage::PrefetchStarted`] and
/// [`messages::RunnerMessage::PrefetchCompleted`].
///
/// This is the single-crate counterpart to [`Cgx::prefetch_all`]. `label` is the user-facing
/// identifier shown in those messages: the raw CLI crate spec (e.g. `ripgrep@1.0`), the
/// resolved crate name, or a `<source>` placeholder for source-only invocations.
pub fn prefetch(&self, label: &str, request: &CrateRequest, overrides: &BuildOverrides) -> Result<()> {
self.reporter
.report(|| messages::RunnerMessage::prefetch_started(label));
let bin_path = self.prepare_bin_crate(request, overrides)?;
self.reporter
.report(|| messages::RunnerMessage::prefetch_completed(label, &bin_path));
Ok(())
}
/// Prefetch every tool and alias configured in the `[tools]`/`[aliases]` config sections.
///
/// Tools and aliases are grouped by the crate they resolve to, so each distinct crate is
/// prefetched exactly once no matter how many configured names point at it; the configured
/// names are reported alongside it. Every configured tool is attempted even if some fail; if
/// any failed, this returns [`error::Error::PrefetchAllFailed`] listing them.
pub fn prefetch_all(&self, overrides: &BuildOverrides) -> Result<()> {
let mut failures = Vec::new();
for tool in self.config.configured_tools() {
self.reporter
.report(|| messages::RunnerMessage::prefetch_all_started(&tool.name, &tool.aliases));
let request = CrateRequest::for_configured_tool(&tool.name);
match self.prepare_bin_crate(&request, overrides) {
Ok(bin_path) => {
self.reporter.report(|| {
messages::RunnerMessage::prefetch_all_completed(&tool.name, &tool.aliases, &bin_path)
});
}
Err(err) => {
self.reporter.report(|| {
messages::RunnerMessage::prefetch_all_failed(&tool.name, &tool.aliases, &err)
});
failures.push(format!("{}: {}", tool.name, err));
}
}
}
if failures.is_empty() {
Ok(())
} else {
error::PrefetchAllFailedSnafu { failures }.fail()
}
}
/// Load the resolved [`CrateSpec`] and [`BuildOptions`] for a crate request.
///
/// Applies the engine's config and `overrides` to turn a [`CrateRequest`] into the concrete
/// crate spec and build options.
fn resolve_crate_request(
&self,
request: &CrateRequest,
overrides: &BuildOverrides,
) -> Result<(CrateSpec, BuildOptions)> {
let crate_spec = CrateSpec::load(&self.config, request)?;
let build_options = BuildOptions::load_for_crate(&self.config, overrides, &crate_spec)?;
Ok((crate_spec, build_options))
}
/// Resolve and download a crate given an already-resolved [`CrateSpec`], producing a
/// [`downloader::DownloadedCrate`].
///
/// NOTE: This is downloading the crate source code, which must be done even if we eventually
/// end up selecting a prebuilt binary instead of building from source.
fn resolve_and_download_crate_spec(
&self,
crate_spec: &CrateSpec,
build_options: &BuildOptions,
) -> Result<downloader::DownloadedCrate> {
tracing::debug!("Got crate spec: {:?}", crate_spec);
tracing::info!("Resolving crate...");
let resolved_crate = self.resolver.resolve(crate_spec)?;
tracing::info!(
"Resolved crate {}@{}",
resolved_crate.name,
resolved_crate.version
);
let downloaded_crate = self.downloader.download(resolved_crate)?;
tracing::debug!("Downloaded crate to cache: {:#?}", downloaded_crate);
self.reporter.report(|| {
messages::CgxMessage::crate_plan(
&downloaded_crate.resolved,
&downloaded_crate.crate_path,
build_options,
)
});
Ok(downloaded_crate)
}
}