Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions docs/source/builder/local-dev.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ $ cargo install hf-kernel-builder

## Generating a Python project with `kernel-builder`

`kernel-builder` generates a CMake/Python project from a [`build.toml`](./writing-kernels.md)
file. The invocation is as follows:
`kernel-builder` can create CMake/Python project files for a kernel with
a [`build.toml`](./writing-kernels.md) file. The `create-pyproject`
command will create the files for the kernel in the current directory:

```bash
$ kernel-builder create-pyproject build.toml -f
$ kernel-builder create-pyproject -f
```

The `-f` flag is optional and instructs `kernel-builder` to overwrite
Expand All @@ -43,6 +44,12 @@ $ pip install wheel # Needed once to enable bdist_wheel.
$ pip install --no-build-isolation -e .
```

You can also create a Python project for a kernel in another directory:

```bash
$ kernel-builder create-pyproject -f path/to/kernel
```

**Warnings:**

- Kernels built in this way should **not** be published on the Kernel
Expand Down
4 changes: 2 additions & 2 deletions docs/source/builder/nix.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ project files. For example:

```bash
$ nix develop
$ kernel-builder create-pyproject build.toml
$ kernel-builder create-pyproject
$ cmake -B build-ext
$ cmake --build build-ext
```
Expand All @@ -74,7 +74,7 @@ Python package in this virtual environment:

```bash
$ nix develop
$ kernel-builder create-pyproject build.toml
$ kernel-builder create-pyproject
$ pip install --no-build-isolation -e .
```

Expand Down
226 changes: 30 additions & 196 deletions kernel-builder/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
use std::{
fs::{self, File},
io::{BufWriter, Read, Write},
path::{Path, PathBuf},
};
use std::fs::File;
use std::io::{BufWriter, Write};
use std::path::PathBuf;

use clap::{Parser, Subcommand};
use eyre::{bail, ensure, Context, Result};
use eyre::{Context, Result};

mod pyproject;
use pyproject::create_pyproject_file_set;
use pyproject::{clean_pyproject, create_pyproject};

mod config;
use config::{v3, Build, BuildCompat};

mod util;
use util::{check_or_infer_kernel_dir, parse_and_validate};

mod version;

#[derive(Parser, Debug)]
Expand All @@ -26,8 +27,8 @@ struct Cli {
enum Commands {
/// Generate CMake files for a kernel extension build.
CreatePyproject {
#[arg(name = "BUILD_TOML")]
build_toml: PathBuf,
#[arg(name = "KERNEL_DIR")]
kernel_dir: Option<PathBuf>,

/// The directory to write the generated files to
/// (directory of `BUILD_TOML` when absent).
Expand All @@ -46,20 +47,20 @@ enum Commands {

/// Update a `build.toml` to the current format.
UpdateBuild {
#[arg(name = "BUILD_TOML")]
build_toml: PathBuf,
#[arg(name = "KERNEL_DIR")]
kernel_dir: Option<PathBuf>,
},

/// Validate the build.toml file.
Validate {
#[arg(name = "BUILD_TOML")]
build_toml: PathBuf,
#[arg(name = "KERNEL_DIR")]
kernel_dir: Option<PathBuf>,
},

/// Clean generated artifacts.
CleanPyproject {
#[arg(name = "BUILD_TOML")]
build_toml: PathBuf,
#[arg(name = "KERNEL_DIR")]
kernel_dir: Option<PathBuf>,

/// The directory to clean from (directory of `BUILD_TOML` when absent).
#[arg(name = "TARGET_DIR")]
Expand All @@ -84,58 +85,35 @@ fn main() -> Result<()> {
let args = Cli::parse();
match args.command {
Commands::CreatePyproject {
build_toml,
kernel_dir,
force,
target_dir,
ops_id,
} => create_pyproject(build_toml, target_dir, force, ops_id),
Commands::UpdateBuild { build_toml } => update_build(build_toml),
Commands::Validate { build_toml } => {
parse_and_validate(build_toml)?;
} => create_pyproject(kernel_dir, target_dir, force, ops_id),
Commands::UpdateBuild { kernel_dir } => update_build(kernel_dir),
Commands::Validate { kernel_dir } => {
validate(kernel_dir)?;
Ok(())
}
Commands::CleanPyproject {
build_toml,
kernel_dir,
target_dir,
dry_run,
force,
ops_id,
} => clean_pyproject(build_toml, target_dir, dry_run, force, ops_id),
} => clean_pyproject(kernel_dir, target_dir, dry_run, force, ops_id),
}
}

fn parse_build(build_toml: impl AsRef<Path>) -> Result<Build> {
let build_compat = parse_and_validate(build_toml)?;

if matches!(build_compat, BuildCompat::V1(_) | BuildCompat::V2(_)) {
eprintln!(
"build.toml is in the deprecated V1 or V2 format, use `kernel-builder update-build` to update."
)
}

let build: Build = build_compat
.try_into()
.context("Cannot update build configuration")?;

Ok(build)
}

fn create_pyproject(
build_toml: PathBuf,
target_dir: Option<PathBuf>,
force: bool,
ops_id: Option<String>,
) -> Result<()> {
let target_dir = check_or_infer_target_dir(&build_toml, target_dir)?;
let build = parse_build(&build_toml)?;
let file_set = create_pyproject_file_set(build, &target_dir, ops_id)?;
file_set.write(&target_dir, force)?;

fn validate(kernel_dir: Option<PathBuf>) -> Result<()> {
let kernel_dir = check_or_infer_kernel_dir(kernel_dir)?;
parse_and_validate(kernel_dir)?;
Ok(())
}

fn update_build(build_toml: PathBuf) -> Result<()> {
let build_compat: BuildCompat = parse_and_validate(&build_toml)?;
fn update_build(kernel_dir: Option<PathBuf>) -> Result<()> {
let kernel_dir = check_or_infer_kernel_dir(kernel_dir)?;
let build_compat: BuildCompat = parse_and_validate(&kernel_dir)?;

if matches!(build_compat, BuildCompat::V3(_)) {
return Ok(());
Expand All @@ -147,6 +125,7 @@ fn update_build(build_toml: PathBuf) -> Result<()> {
let v3_build: v3::Build = build.into();
let pretty_toml = toml::to_string_pretty(&v3_build)?;

let build_toml = kernel_dir.join("build.toml");
let mut writer =
BufWriter::new(File::create(&build_toml).wrap_err_with(|| {
format!("Cannot open {} for writing", build_toml.to_string_lossy())
Expand All @@ -157,148 +136,3 @@ fn update_build(build_toml: PathBuf) -> Result<()> {

Ok(())
}

fn check_or_infer_target_dir(
build_toml: impl AsRef<Path>,
target_dir: Option<PathBuf>,
) -> Result<PathBuf> {
let build_toml = build_toml.as_ref();
match target_dir {
Some(target_dir) => {
ensure!(
target_dir.is_dir(),
"`{}` is not a directory",
target_dir.to_string_lossy()
);
Ok(target_dir)
}
None => {
let absolute = std::path::absolute(build_toml)?;
match absolute.parent() {
Some(parent) => Ok(parent.to_owned()),
None => bail!(
"Cannot get parent path of `{}`",
build_toml.to_string_lossy()
),
}
}
}
}

fn parse_and_validate(build_toml: impl AsRef<Path>) -> Result<BuildCompat> {
let build_toml = build_toml.as_ref();
let mut toml_data = String::new();
File::open(build_toml)
.wrap_err_with(|| format!("Cannot open {} for reading", build_toml.to_string_lossy()))?
.read_to_string(&mut toml_data)
.wrap_err_with(|| format!("Cannot read from {}", build_toml.to_string_lossy()))?;

let build_compat: BuildCompat = toml::from_str(&toml_data)
.wrap_err_with(|| format!("Cannot parse TOML in {}", build_toml.to_string_lossy()))?;

Ok(build_compat)
}

fn clean_pyproject(
build_toml: PathBuf,
target_dir: Option<PathBuf>,
dry_run: bool,
force: bool,
ops_id: Option<String>,
) -> Result<()> {
let target_dir = check_or_infer_target_dir(&build_toml, target_dir)?;

let build = parse_build(&build_toml)?;
let generated_files =
create_pyproject_file_set(build, target_dir.clone(), ops_id)?.into_names();

if generated_files.is_empty() {
eprintln!("No generated artifacts found to clean.");
return Ok(());
}

if dry_run {
println!("Files that would be deleted:");
for file in &generated_files {
if file.exists() {
println!(" {}", file.to_string_lossy());
}
}
return Ok(());
}

let existing_files: Vec<_> = generated_files.iter().filter(|f| f.exists()).collect();

if existing_files.is_empty() {
eprintln!("No generated artifacts found to clean.");
return Ok(());
}

if !force {
println!("Files to be deleted:");
for file in &existing_files {
println!(" {}", file.to_string_lossy());
}
print!("Continue? [y/N] ");
std::io::stdout().flush()?;

let mut response = String::new();
std::io::stdin().read_line(&mut response)?;
let response = response.trim().to_lowercase();

if response != "y" && response != "yes" {
eprintln!("Aborted.");
return Ok(());
}
}

let mut deleted_count = 0;
let mut errors = Vec::new();

for file in existing_files {
match fs::remove_file(file) {
Ok(_) => {
deleted_count += 1;
println!("Deleted: {}", file.to_string_lossy());
}
Err(e) => {
errors.push(format!(
"Failed to delete {}: {}",
file.to_string_lossy(),
e
));
}
}
}

// Clean up empty directories
let dirs_to_check = [target_dir.join("cmake")];

for dir in dirs_to_check {
if dir.exists() && is_empty_dir(&dir)? {
match fs::remove_dir(&dir) {
Ok(_) => println!("Removed empty directory: {}", dir.to_string_lossy()),
Err(e) => eyre::bail!("Failed to remove directory `{}`: {e:?}", dir.display()),
}
}
}

if !errors.is_empty() {
for error in errors {
eprintln!("Error: {error}");
}
bail!("Some files could not be deleted");
}

println!("Cleaned {deleted_count} generated artifacts.");
Ok(())
}

fn is_empty_dir(dir: &Path) -> Result<bool> {
if !dir.is_dir() {
return Ok(false);
}

let mut entries = fs::read_dir(dir)?;
Ok(entries.next().is_none())
}
Loading
Loading