Skip to main content

cgx_core/
lib.rs

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