Skip to content

Commit b497234

Browse files
authored
Make cargo-gpu a library, usable from build scripts (#71)
* build script: move Command run logic to `.run()` * move usage dumping for readme to `mod dump_usage` * move testing utilities to `mod test` * turn cargo-gpu into a pure library * recreate the binary and main function * fix clippy warnings * make Install usable from a lib * fix clippy * make dummy a library, skip linking an executable binary * add logging on target-specs writing * deduplicate cargo cloning rust-gpu due to slightly different url * change unreachable Err handling * make `Install` and `InstalledBackend` `#[non_exhaustive]`
1 parent 8087e22 commit b497234

File tree

9 files changed

+406
-346
lines changed

9 files changed

+406
-346
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ exclude = [
1313
resolver = "2"
1414

1515
[workspace.dependencies]
16-
spirv-builder = { git = "https://github.com/Rust-GPU/rust-gpu.git", rev = "86fc48032c4cd4afb74f1d81ae859711d20386a1", default-features = false }
16+
spirv-builder = { git = "https://github.com/Rust-GPU/rust-gpu", rev = "86fc48032c4cd4afb74f1d81ae859711d20386a1", default-features = false }
1717
anyhow = "1.0.94"
1818
clap = { version = "4.5.37", features = ["derive"] }
1919
crossterm = "0.28.1"

crates/cargo-gpu/src/dump_usage.rs

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
//! Convenience function for internal use. Dumps all the CLI usage instructions. Useful for
2+
//! updating the README.
3+
4+
use crate::{user_output, Cli};
5+
6+
/// main dump usage function
7+
pub fn dump_full_usage_for_readme() -> anyhow::Result<()> {
8+
use clap::CommandFactory as _;
9+
let mut command = Cli::command();
10+
11+
let mut buffer: Vec<u8> = Vec::default();
12+
command.build();
13+
14+
write_help(&mut buffer, &mut command, 0)?;
15+
user_output!("{}", String::from_utf8(buffer)?);
16+
17+
Ok(())
18+
}
19+
20+
/// Recursive function to print the usage instructions for each subcommand.
21+
fn write_help(
22+
buffer: &mut impl std::io::Write,
23+
cmd: &mut clap::Command,
24+
depth: usize,
25+
) -> anyhow::Result<()> {
26+
if cmd.get_name() == "help" {
27+
return Ok(());
28+
}
29+
30+
let mut command = cmd.get_name().to_owned();
31+
let indent_depth = if depth == 0 || depth == 1 { 0 } else { depth };
32+
let indent = " ".repeat(indent_depth * 4);
33+
writeln!(
34+
buffer,
35+
"\n{}* {}{}",
36+
indent,
37+
command.remove(0).to_uppercase(),
38+
command
39+
)?;
40+
41+
for line in cmd.render_long_help().to_string().lines() {
42+
writeln!(buffer, "{indent} {line}")?;
43+
}
44+
45+
for sub in cmd.get_subcommands_mut() {
46+
writeln!(buffer)?;
47+
write_help(buffer, sub, depth + 1)?;
48+
}
49+
50+
Ok(())
51+
}

crates/cargo-gpu/src/install.rs

Lines changed: 86 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,63 @@ use crate::spirv_source::{
77
use crate::{cache_dir, spirv_source::SpirvSource};
88
use anyhow::Context as _;
99
use cargo_metadata::Metadata;
10-
use log::{info, trace};
1110
use spirv_builder::SpirvBuilder;
1211
use std::path::{Path, PathBuf};
1312

13+
/// Represents a functional backend installation, whether it was cached or just installed.
14+
#[derive(Clone, Debug)]
15+
#[non_exhaustive]
16+
pub struct InstalledBackend {
17+
/// path to the `rustc_codegen_spirv` dylib
18+
pub rustc_codegen_spirv_location: PathBuf,
19+
/// toolchain channel name
20+
pub toolchain_channel: String,
21+
/// directory with target-specs json files
22+
pub target_spec_dir: PathBuf,
23+
}
24+
25+
impl InstalledBackend {
26+
/// Creates a new `SpirvBuilder` configured to use this installed backend.
27+
#[expect(
28+
clippy::unreachable,
29+
reason = "it's unreachable, no need to return a Result"
30+
)]
31+
#[expect(clippy::impl_trait_in_params, reason = "forwarding spirv-builder API")]
32+
#[inline]
33+
pub fn to_spirv_builder(
34+
&self,
35+
path_to_crate: impl AsRef<Path>,
36+
target: impl Into<String>,
37+
) -> SpirvBuilder {
38+
let mut builder = SpirvBuilder::new(path_to_crate, target);
39+
self.configure_spirv_builder(&mut builder)
40+
.unwrap_or_else(|_| unreachable!("we set target before calling this function"));
41+
builder
42+
}
43+
44+
/// Configures the supplied [`SpirvBuilder`]. `SpirvBuilder.target` must be set and must not change after calling this function.
45+
///
46+
/// # Errors
47+
/// if `SpirvBuilder.target` is not set
48+
#[inline]
49+
pub fn configure_spirv_builder(&self, builder: &mut SpirvBuilder) -> anyhow::Result<()> {
50+
builder.rustc_codegen_spirv_location = Some(self.rustc_codegen_spirv_location.clone());
51+
builder.toolchain_overwrite = Some(self.toolchain_channel.clone());
52+
builder.path_to_target_spec = Some(self.target_spec_dir.join(format!(
53+
"{}.json",
54+
builder.target.as_ref().context("expect target to be set")?
55+
)));
56+
Ok(())
57+
}
58+
}
59+
1460
/// Args for an install
1561
#[expect(
1662
clippy::struct_excessive_bools,
1763
reason = "cmdline args have many bools"
1864
)]
1965
#[derive(clap::Parser, Debug, Clone, serde::Deserialize, serde::Serialize)]
66+
#[non_exhaustive]
2067
pub struct Install {
2168
/// Directory containing the shader crate to compile.
2269
#[clap(long, default_value = "./")]
@@ -44,6 +91,8 @@ pub struct Install {
4491
pub rebuild_codegen: bool,
4592

4693
/// Assume "yes" to "Install Rust toolchain: [y/n]" prompt.
94+
///
95+
/// Defaults to `false` in cli, `true` in [`Default`]
4796
#[clap(long, action)]
4897
pub auto_install_rust_toolchain: bool,
4998

@@ -77,46 +126,22 @@ pub struct Install {
77126
pub force_overwrite_lockfiles_v4_to_v3: bool,
78127
}
79128

80-
/// Represents a functional backend installation, whether it was cached or just installed.
81-
#[derive(Clone, Debug)]
82-
pub struct InstalledBackend {
83-
/// path to the `rustc_codegen_spirv` dylib
84-
pub rustc_codegen_spirv_location: PathBuf,
85-
/// toolchain channel name
86-
pub toolchain_channel: String,
87-
/// directory with target-specs json files
88-
pub target_spec_dir: PathBuf,
89-
}
90-
91-
impl InstalledBackend {
92-
/// Configures the supplied [`SpirvBuilder`]. `SpirvBuilder.target` must be set and must not change after calling this function.
93-
pub fn configure_spirv_builder(&self, builder: &mut SpirvBuilder) -> anyhow::Result<()> {
94-
builder.rustc_codegen_spirv_location = Some(self.rustc_codegen_spirv_location.clone());
95-
builder.toolchain_overwrite = Some(self.toolchain_channel.clone());
96-
builder.path_to_target_spec = Some(self.target_spec_dir.join(format!(
97-
"{}.json",
98-
builder.target.as_ref().context("expect target to be set")?
99-
)));
100-
Ok(())
101-
}
102-
}
103-
104-
impl Default for Install {
129+
impl Install {
130+
/// Create a default install for a shader crate of some path
105131
#[inline]
106-
fn default() -> Self {
132+
#[must_use]
133+
pub const fn from_shader_crate(shader_crate: PathBuf) -> Self {
107134
Self {
108-
shader_crate: PathBuf::from("./"),
135+
shader_crate,
109136
spirv_builder_source: None,
110137
spirv_builder_version: None,
111138
rebuild_codegen: false,
112-
auto_install_rust_toolchain: false,
139+
auto_install_rust_toolchain: true,
113140
clear_target: true,
114141
force_overwrite_lockfiles_v4_to_v3: false,
115142
}
116143
}
117-
}
118144

119-
impl Install {
120145
/// Create the `rustc_codegen_spirv_dummy` crate that depends on `rustc_codegen_spirv`
121146
fn write_source_files(source: &SpirvSource, checkout: &Path) -> anyhow::Result<()> {
122147
// skip writing a dummy project if we use a local rust-gpu checkout
@@ -129,15 +154,14 @@ impl Install {
129154
);
130155

131156
{
132-
trace!("writing dummy main.rs");
133-
let main = "fn main() {}";
157+
log::trace!("writing dummy lib.rs");
134158
let src = checkout.join("src");
135-
std::fs::create_dir_all(&src).context("creating directory for 'src'")?;
136-
std::fs::write(src.join("main.rs"), main).context("writing 'main.rs'")?;
159+
std::fs::create_dir_all(&src).context("creating 'src' directory")?;
160+
std::fs::File::create(src.join("lib.rs")).context("creating 'src/lib.rs'")?;
137161
};
138162

139163
{
140-
trace!("writing dummy Cargo.toml");
164+
log::trace!("writing dummy Cargo.toml");
141165
let version_spec = match &source {
142166
SpirvSource::CratesIO(version) => {
143167
format!("version = \"{version}\"")
@@ -174,11 +198,6 @@ package = "rustc_codegen_spirv"
174198

175199
/// Copy spec files from one dir to another, assuming no subdirectories
176200
fn copy_spec_files(src: &Path, dst: &Path) -> anyhow::Result<()> {
177-
info!(
178-
"Copy target specs from {:?} to {:?}",
179-
src.display(),
180-
dst.display()
181-
);
182201
std::fs::create_dir_all(dst)?;
183202
let dir = std::fs::read_dir(src)?;
184203
for dir_entry in dir {
@@ -193,7 +212,6 @@ package = "rustc_codegen_spirv"
193212

194213
/// Add the target spec files to the crate.
195214
fn update_spec_files(
196-
&self,
197215
source: &SpirvSource,
198216
install_dir: &Path,
199217
dummy_metadata: &Metadata,
@@ -204,6 +222,11 @@ package = "rustc_codegen_spirv"
204222
if let Ok(target_specs) =
205223
dummy_metadata.find_package("rustc_codegen_spirv-target-specs")
206224
{
225+
log::info!(
226+
"target-specs: found crate `rustc_codegen_spirv-target-specs` with manifest at `{}`",
227+
target_specs.manifest_path
228+
);
229+
207230
let target_specs_src = target_specs
208231
.manifest_path
209232
.as_std_path()
@@ -215,9 +238,17 @@ package = "rustc_codegen_spirv"
215238
.context("Could not find `target-specs` directory within `rustc_codegen_spirv-target-specs` dependency")?;
216239
if source.is_path() {
217240
// skip copy
241+
log::info!(
242+
"target-specs: source is local path, use target-specs from `{}`",
243+
target_specs_src.display()
244+
);
218245
target_specs_dst = target_specs_src;
219246
} else {
220247
// copy over the target-specs
248+
log::info!(
249+
"target-specs: Copy target specs from `{}`",
250+
target_specs_src.display()
251+
);
221252
Self::copy_spec_files(&target_specs_src, &target_specs_dst)
222253
.context("copying target-specs json files")?;
223254
}
@@ -231,14 +262,22 @@ package = "rustc_codegen_spirv"
231262
// and hope parallel runs don't shred each other
232263
target_specs_dst = cache_dir()?.join("legacy-target-specs-for-local-checkout");
233264
}
234-
write_legacy_target_specs(&target_specs_dst, self.rebuild_codegen)?;
265+
log::info!(
266+
"target-specs: Writing legacy target specs to `{}`",
267+
target_specs_dst.display()
268+
);
269+
write_legacy_target_specs(&target_specs_dst)?;
235270
}
236271
}
237272

238273
Ok(target_specs_dst)
239274
}
240275

241-
/// Install the binary pair and return the `(dylib_path, toolchain_channel)`.
276+
/// Install the binary pair and return the [`InstalledBackend`], from which you can create [`SpirvBuilder`] instances.
277+
///
278+
/// # Errors
279+
/// If the installation somehow fails.
280+
#[inline]
242281
#[expect(clippy::too_many_lines, reason = "it's fine")]
243282
pub fn run(&self) -> anyhow::Result<InstalledBackend> {
244283
// Ensure the cache dir exists
@@ -299,9 +338,9 @@ package = "rustc_codegen_spirv"
299338
log::info!("selected toolchain channel `{toolchain_channel:?}`");
300339

301340
log::debug!("update_spec_files");
302-
let target_spec_dir = self
303-
.update_spec_files(&source, &install_dir, &dummy_metadata, skip_rebuild)
304-
.context("writing target spec files")?;
341+
let target_spec_dir =
342+
Self::update_spec_files(&source, &install_dir, &dummy_metadata, skip_rebuild)
343+
.context("writing target spec files")?;
305344

306345
if !skip_rebuild {
307346
log::debug!("ensure_toolchain_and_components_exist");

crates/cargo-gpu/src/legacy_target_specs.rs

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,23 +4,15 @@
44
//! introduced before the first target spec update.
55
66
use anyhow::Context as _;
7-
use log::info;
87
use std::path::Path;
98

109
/// Extract legacy target specs from our executable into some directory
11-
pub fn write_legacy_target_specs(target_spec_dir: &Path, rebuild: bool) -> anyhow::Result<()> {
12-
info!(
13-
"Writing legacy target specs to {}",
14-
target_spec_dir.display()
15-
);
10+
pub fn write_legacy_target_specs(target_spec_dir: &Path) -> anyhow::Result<()> {
1611
std::fs::create_dir_all(target_spec_dir)?;
1712
for (filename, contents) in legacy_target_specs::TARGET_SPECS {
1813
let path = target_spec_dir.join(filename);
19-
if !path.is_file() || rebuild {
20-
std::fs::write(&path, contents.as_bytes()).with_context(|| {
21-
format!("writing legacy target spec file at [{}]", path.display())
22-
})?;
23-
}
14+
std::fs::write(&path, contents.as_bytes())
15+
.with_context(|| format!("writing legacy target spec file at [{}]", path.display()))?;
2416
}
2517
Ok(())
2618
}

0 commit comments

Comments
 (0)