Skip to content

Commit 9f1b468

Browse files
committed
chore: Split crate into bin and lib targets
Splits the crate into separate bin and lib targets to make it easier to write integration tests.
1 parent cbc18d1 commit 9f1b468

10 files changed

Lines changed: 848 additions & 805 deletions

File tree

Cargo.lock

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,11 @@ anyhow = "1.0.100"
2828
[profile.release]
2929
debug = "full"
3030
strip = "none"
31+
32+
[lib]
33+
name = "cargo_subspace"
34+
path = "src/lib.rs"
35+
36+
[[bin]]
37+
name = "cargo-subspace"
38+
path = "src/main.rs"

src/cli.rs

Lines changed: 23 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,55 +14,44 @@ pub struct CargoSubspace {
1414
#[arg(long, short)]
1515
pub verbose: bool,
1616

17-
/// The explicit path to the directory containing your cargo binaries. By default,
18-
/// `cargo-subspace` will use the binaries on your `PATH`.
19-
#[arg(long, env = "CARGO_HOME")]
20-
pub cargo_home: Option<PathBuf>,
21-
2217
/// The location where log files will be stored.
2318
///
2419
/// Default: $HOME/.local/state/cargo-subspace/cargo-subspace.log
2520
#[arg(long)]
2621
pub log_location: Option<PathBuf>,
2722

28-
#[command(subcommand)]
29-
pub command: SubspaceCommand,
30-
}
31-
32-
#[derive(PartialEq, Clone, Debug, Parser)]
33-
pub struct FeatureArgs {
34-
/// Activate all features in the workspace.
35-
///
36-
/// Note that this flag applies to the whole workspace, not just the crate you're currently
37-
/// working on.
38-
#[arg(long, conflicts_with = "no_default_features")]
39-
pub all_features: bool,
40-
41-
/// Don't include default features during the workspace discovery process.
23+
/// The explicit path to your cargo home. Usually, this is `$HOME/.cargo`.
4224
///
43-
/// Note that this flag applies to the whole workspace, not just the crate you're currently
44-
/// working on.
45-
#[arg(long, conflicts_with = "all_features")]
46-
pub no_default_features: bool,
47-
}
48-
49-
#[derive(PartialEq, Clone, Debug, Parser)]
50-
pub struct DiscoverArgs {
51-
/// Profiles the discover process and writes a flamegraph to the given path
52-
#[arg(long, hide = true)]
53-
pub flamegraph: Option<PathBuf>,
25+
/// If this is unset, `cargo-subspace` will use the binaries on your `PATH`.
26+
#[arg(long, env = "CARGO_HOME")]
27+
pub cargo_home: Option<PathBuf>,
5428

55-
#[command(flatten)]
56-
pub feature_args: FeatureArgs,
29+
#[command(subcommand)]
30+
pub command: SubspaceCommand,
5731
}
5832

5933
#[derive(PartialEq, Clone, Debug, Parser)]
6034
pub enum SubspaceCommand {
6135
/// Print the cargo-subspace version and sysroot path and exit
6236
Version,
6337
Discover {
64-
#[command(flatten)]
65-
discover_args: DiscoverArgs,
38+
/// Activate all features in the workspace.
39+
///
40+
/// Note that this flag applies to the whole workspace, not just the crate you're currently
41+
/// working on.
42+
#[arg(long, conflicts_with = "no_default_features")]
43+
all_features: bool,
44+
45+
/// Don't include default features during the workspace discovery process.
46+
///
47+
/// Note that this flag applies to the whole workspace, not just the crate you're currently
48+
/// working on.
49+
#[arg(long, conflicts_with = "all_features")]
50+
no_default_features: bool,
51+
52+
/// Profiles the discover process and writes a flamegraph to the given path
53+
#[arg(long, hide = true)]
54+
flamegraph: Option<PathBuf>,
6655

6756
arg: DiscoverArgument,
6857
},

src/discover.rs

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
use std::{
2+
io::{BufRead, BufReader},
3+
path::PathBuf,
4+
process::{Command, Stdio},
5+
};
6+
7+
use anyhow::{Result, anyhow};
8+
use cargo_metadata::{Artifact, Message, Metadata, MetadataCommand, camino::Utf8PathBuf};
9+
use tracing::debug;
10+
11+
use crate::{graph::CrateGraph, log_progress, rust_project::ProjectJson, util::FilePathBuf};
12+
13+
pub struct DiscoverRunner {
14+
toolchain: Toolchain,
15+
features: FeatureOption,
16+
manifest_path: FilePathBuf,
17+
}
18+
19+
impl DiscoverRunner {
20+
pub fn from_manifest_path(manifest_path: FilePathBuf) -> Self {
21+
Self {
22+
manifest_path,
23+
toolchain: Toolchain::default(),
24+
features: FeatureOption::Default,
25+
}
26+
}
27+
28+
pub fn from_src_file(src_file: FilePathBuf) -> Result<Self> {
29+
let manifest_path = crate::find_manifest(src_file)?;
30+
Ok(Self::from_manifest_path(manifest_path))
31+
}
32+
33+
pub fn with_all_features(mut self) -> Self {
34+
self.features = FeatureOption::All;
35+
self
36+
}
37+
38+
pub fn with_no_default_features(mut self) -> Self {
39+
self.features = FeatureOption::NoDefault;
40+
self
41+
}
42+
43+
pub fn with_default_features(mut self) -> Self {
44+
self.features = FeatureOption::Default;
45+
self
46+
}
47+
48+
pub fn with_cargo_home(mut self, cargo_home: Option<PathBuf>) -> Self {
49+
self.toolchain = Toolchain { cargo_home };
50+
self
51+
}
52+
53+
pub fn get_crate_graph(&self) -> Result<CrateGraph> {
54+
// Get the cargo workspace metadata
55+
let metadata = self.get_metadata()?;
56+
57+
// Lower the metadata into our internal crate graph representation
58+
let mut graph = CrateGraph::from_metadata(metadata)?;
59+
60+
// Prune the graph such that the remaining nodes are only those reachable from the node
61+
// with the given manifest path
62+
graph.prune(self.manifest_path.as_file_path())?;
63+
64+
Ok(graph)
65+
}
66+
67+
/// Fetches the cargo metadata, prunes the crate graph based on the given manifest path, and
68+
/// returns the graph in the format expected by rust-analyzer
69+
pub fn run(self) -> Result<(ProjectJson, FilePathBuf)> {
70+
let mut graph = self.get_crate_graph()?;
71+
72+
// Build the compile time dependencies (proc macros & build scripts) for the pruned graph
73+
self.build_compile_time_dependencies(&mut graph)?;
74+
75+
// Convert the crate graph into a flat list of crates
76+
let crates = graph.into_crates()?;
77+
78+
let p: PathBuf = String::from_utf8(
79+
self.toolchain
80+
.rustc()
81+
.arg("--print")
82+
.arg("sysroot")
83+
.output()?
84+
.stdout,
85+
)?
86+
.trim()
87+
.into();
88+
89+
let sysroot = Utf8PathBuf::from_path_buf(p)
90+
.map_err(|_| anyhow!("Path contains non-UTF-8 characters"))?;
91+
debug!(sysroot = %sysroot);
92+
93+
let sysroot_src = sysroot.join("lib/rustlib/src/rust/library");
94+
95+
let root = self
96+
.toolchain
97+
.cargo()
98+
.arg("locate-project")
99+
.arg("--workspace")
100+
.arg("--manifest-path")
101+
.arg(self.manifest_path.as_std_path())
102+
.arg("--message-format")
103+
.arg("plain")
104+
.output()?;
105+
let buildfile: PathBuf = String::from_utf8(root.stdout)?.trim().into();
106+
107+
let project = ProjectJson {
108+
sysroot,
109+
sysroot_src: Some(sysroot_src),
110+
// TODO: do i need this? buck excludes it...
111+
// sysroot_project: None,
112+
// TODO: do i need this? buck excludes it...
113+
// cfg_groups: HashMap::new(),
114+
crates,
115+
// TODO: Add support for runnables
116+
runnables: vec![],
117+
};
118+
119+
Ok((project, buildfile.try_into()?))
120+
}
121+
122+
fn get_metadata(&self) -> Result<Metadata> {
123+
log_progress("Fetching metadata")?;
124+
125+
let rustc_info = String::from_utf8(self.toolchain.rustc().arg("-vV").output()?.stdout)?;
126+
let mut cmd = MetadataCommand::new();
127+
cmd.manifest_path(self.manifest_path.as_std_path());
128+
129+
if let Some(cargo_home) = self.toolchain.cargo_home.as_ref() {
130+
cmd.cargo_path(cargo_home.join("bin/cargo"));
131+
}
132+
133+
let target_triple = rustc_info
134+
.lines()
135+
.find_map(|line| line.strip_prefix("host: "));
136+
if let Some(target_triple) = target_triple {
137+
cmd.other_options(["--filter-platform".into(), target_triple.into()]);
138+
}
139+
140+
match self.features {
141+
FeatureOption::All => {
142+
cmd.features(cargo_metadata::CargoOpt::AllFeatures);
143+
}
144+
FeatureOption::NoDefault => {
145+
cmd.features(cargo_metadata::CargoOpt::NoDefaultFeatures);
146+
}
147+
FeatureOption::Default => (),
148+
}
149+
150+
Ok(cmd.exec()?)
151+
}
152+
153+
fn build_compile_time_dependencies(&self, graph: &mut CrateGraph) -> Result<()> {
154+
// TODO: check rust version to decide whether to use --compile-time-deps, which allows us to
155+
// only build proc macros/build scripts during this step instead of building the whole crate
156+
let child = self
157+
.toolchain
158+
.cargo()
159+
// .arg("+nightly")
160+
.arg("check")
161+
// .arg("--compile-time-deps")
162+
.arg("--quiet")
163+
.arg("--message-format")
164+
.arg("json")
165+
.arg("--keep-going")
166+
.arg("--all-targets")
167+
.arg("--manifest-path")
168+
.arg(self.manifest_path.as_std_path())
169+
// .arg("-Zunstable-options")
170+
// .env("__CARGO_TEST_CHANNEL_OVERRIDE_DO_NOT_USE_THIS", "nightly")
171+
.stdout(Stdio::piped())
172+
.stderr(Stdio::null())
173+
.spawn()?;
174+
175+
for line in BufReader::new(child.stdout.unwrap()).lines() {
176+
let line = line?;
177+
let message = serde_json::from_str::<Message>(&line)?;
178+
179+
match message {
180+
Message::CompilerArtifact(Artifact {
181+
filenames,
182+
target,
183+
package_id,
184+
..
185+
}) => {
186+
if let Some(dylib) = filenames.into_iter().find(is_dylib)
187+
&& target.is_proc_macro()
188+
{
189+
log_progress(format!("proc-macro {} built", target.name))?;
190+
if let Some(pkg) = graph.get_mut(&package_id) {
191+
pkg.proc_macro_dylib = Some(dylib.try_into()?);
192+
}
193+
}
194+
}
195+
Message::BuildScriptExecuted(script) => {
196+
if let Some(pkg) = graph.get_mut(&script.package_id) {
197+
log_progress(format!("build script {} run", pkg.name))?;
198+
pkg.build_script = Some(script);
199+
}
200+
}
201+
_ => (),
202+
}
203+
}
204+
205+
Ok(())
206+
}
207+
}
208+
209+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
210+
enum FeatureOption {
211+
NoDefault,
212+
All,
213+
Default,
214+
}
215+
216+
#[derive(Default)]
217+
struct Toolchain {
218+
cargo_home: Option<PathBuf>,
219+
}
220+
221+
impl Toolchain {
222+
fn cargo_command(&self, cmd: &str) -> Command {
223+
if let Some(cargo_home) = self.cargo_home.as_ref() {
224+
Command::new(cargo_home.join("bin").join(cmd))
225+
} else {
226+
Command::new(cmd)
227+
}
228+
}
229+
230+
fn rustc(&self) -> Command {
231+
self.cargo_command("rustc")
232+
}
233+
234+
fn cargo(&self) -> Command {
235+
self.cargo_command("cargo")
236+
}
237+
}
238+
239+
fn is_dylib(path: &Utf8PathBuf) -> bool {
240+
path.extension()
241+
.map(|ext| ["dylib", "so", "dll"].contains(&ext))
242+
.unwrap_or(false)
243+
}

0 commit comments

Comments
 (0)