Skip to content
Draft
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
43 changes: 31 additions & 12 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,52 @@
name: Prepare Release

on:
pull_request:
branches:
- main
workflow_dispatch:
inputs:
type:
description: Bump versions and trigger a new release.
required: true
default: release
options:
- release

jobs:
prepare-release:
runs-on: ubuntu-22.04
env:
GITHUB_TOKEN: ${{ secrets.PAT }}
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0
# Release from the ref this was dispatched on, so a prerelease cut from a
# branch tags that branch rather than the default one.
ref: ${{ github.ref }}
token: ${{ secrets.PAT }}
- name: Install Knope
uses: knope-dev/action@v2.1.0
uses: knope-dev/action@v2.1.2
with:
version: 0.10.0 # Test before updating, breaking changes likely: https://github.com/knope-dev/action#install-latest-version
# 0.23 is needed for `--prerelease-label` and for tagging the ref being
# released. 0.10 tagged the default branch instead, so a prerelease cut from
# a branch produced a tag pointing at main's version, and the publish job
# then tried to republish that already-released version.
version: 0.23.0 # Test before updating, breaking changes likely: https://github.com/knope-dev/action#install-latest-version
- run: |
git config --global user.name "${{ github.triggering_actor }}"
git config --global user.email "${{ github.triggering_actor}}@users.noreply.github.com"

# On a pull request, only show what a release would do.
- name: Dry-run Release
if: github.event_name == 'pull_request'
run: knope release --verbose --dry-run

- name: Prepare Release
run: knope ${{ inputs.type }} --verbose
if: github.event_name == 'workflow_dispatch'
env:
# The Release step authenticates with this rather than an argument.
GITHUB_TOKEN: ${{ secrets.PAT }}
run: |
set -euo pipefail

# A release off main is a real one; anywhere else it can only be a
# prerelease, which is how a change that needs a matching libquil release
# gets published for testing before either side is final.
if [[ "$GITHUB_REF" == "refs/heads/main" ]]; then
knope release --verbose
else
knope release --verbose --prerelease-label=rc
fi
15 changes: 14 additions & 1 deletion .github/workflows/release-library.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,20 @@ jobs:
- uses: actions/checkout@v5
with:
fetch-depth: 0
# Publish exactly what was tagged. Without this the checkout follows the
# release's target commitish, which is not necessarily the commit carrying
# the version bump -- a prerelease cut from a branch tagged main, and this
# job then tried to republish main's already-released version.
ref: ${{ github.event.release.tag_name || github.ref }}
token: ${{ secrets.PAT }}
- uses: dtolnay/rust-toolchain@stable
- run: cargo publish --no-verify --manifest-path=lib/Cargo.toml --token ${{ secrets.CRATES_IO_TOKEN }}
- name: Publish to crates.io
run: |
set -euo pipefail

version="$(cargo pkgid --manifest-path=lib/Cargo.toml | sed -E 's|.*[#@]||')"
echo "Publishing libquil-sys $version"

# --no-verify: the crate cannot be built without libquil installed, which
# is not available on this runner.
cargo publish --no-verify --manifest-path=lib/Cargo.toml --token ${{ secrets.CRATES_IO_TOKEN }}
17 changes: 14 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,17 @@ on:
branches:
- 'main'

env:
# The libquil release these bindings are built against. libquil's move to modern
# sbcl-librarian changed its C ABI and the set of files it installs, so this crate
# needs a release from after that change: 0.3.x installs neither the runtime
# headers nor libsbcl_librarian, and the build fails in build.rs.
LIBQUIL_VERSION: "0.4.0-rc.0"
# install.sh is fetched from the tag being installed. It has to match the release
# above: the installer that ships with a release knows which files that release
# contains, and the modern-sbcl-librarian layout added the runtime directory.
LIBQUIL_INSTALL_REF: "v0.4.0-rc.0"

jobs:
test-linux:
runs-on: ubuntu-22.04
Expand All @@ -20,14 +31,14 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- name: Install libquil
run: |
curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | bash -s 0.3.0
curl https://raw.githubusercontent.com/rigetti/libquil/$LIBQUIL_INSTALL_REF/install.sh | bash -s $LIBQUIL_VERSION
- name: Run tests
run: |
cd $GITHUB_WORKSPACE/lib
cargo test

test-macos:
runs-on: macos-15-intel
runs-on: macos-latest
steps:
- uses: actions/checkout@v5
with:
Expand All @@ -37,7 +48,7 @@ jobs:
run: brew install lapack openblas
- uses: dtolnay/rust-toolchain@stable
- name: Install libquil
run: 'curl https://raw.githubusercontent.com/rigetti/libquil/main/install.sh | bash -s 0.3.0'
run: 'curl https://raw.githubusercontent.com/rigetti/libquil/$LIBQUIL_INSTALL_REF/install.sh | bash -s $LIBQUIL_VERSION'
- name: Run tests
run: |
cd $GITHUB_WORKSPACE/lib
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions knope.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ command = "cargo update -w"

[[workflows.steps]]
type = "Command"
shell = true
command = "git add Cargo.lock && git commit -m \"chore: prepare new release(s) [skip ci]\""

[[workflows.steps]]
Expand Down
22 changes: 22 additions & 0 deletions lib/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,25 @@
## 0.5.0-rc.2 (2026-08-12)

### Breaking Changes

- build against libquil on modern sbcl-librarian

### Fixes

- read libquil path envvars at build-script runtime

## 0.5.0-rc.1 (2026-08-12)

### Breaking Changes

- build against libquil on modern sbcl-librarian

## 0.5.0-rc.0 (2026-08-12)

### Breaking Changes

#### build against libquil on modern sbcl-librarian

## 0.4.2 (2026-05-21)

### Fixes
Expand Down
2 changes: 1 addition & 1 deletion lib/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "libquil-sys"
description = "High-level bindings to libquil"
version = "0.4.2"
version = "0.5.0-rc.2"
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/rigetti/libquil-sys"
Expand Down
101 changes: 86 additions & 15 deletions lib/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,42 @@ use std::path::PathBuf;

#[derive(Debug, thiserror::Error)]
enum Error {
#[error("Could not find error in any of the standard locations. Try setting C_INCLUDE_PATH or LIBQUIL_SRC_PATH")]
#[error("Could not find libquil.h in any of the standard locations. Try setting C_INCLUDE_PATH or LIBQUIL_SRC_PATH")]
HeaderNotFound,
#[error(
"Found libquil.h at {0}, but no sbcl_librarian.h beside it. This crate requires a libquil \
built against modern sbcl-librarian, which installs the runtime headers alongside \
libquil.h; an older libquil (0.3.x or earlier) does not have them. Install a newer \
libquil, or set LIBQUIL_SRC_PATH to a build that has one."
)]
RuntimeHeaderNotFound(String),
#[error("Could not read environment variable: {0}")]
InvalidEnvvar(#[from] env::VarError),
}

/// Environment variables that select a libquil installation. They are read when the
/// build script *runs*, so changing one takes effect without a manual `cargo clean`;
/// `cargo:rerun-if-env-changed` is what makes cargo re-run us when they change.
const PATH_ENVVARS: [&str; 3] = ["LIBQUIL_SRC_PATH", "LIBQUIL_LIB_PATH", "C_INCLUDE_PATH"];

/// The value of `name`, or `None` when it is unset or empty. An empty value is
/// treated as unset so that `LIBQUIL_SRC_PATH= cargo build` does not put the
/// current directory at the front of the search order.
fn env_path(name: &str) -> Option<String> {
env::var(name).ok().filter(|value| !value.is_empty())
}

fn get_header_path() -> Result<PathBuf, Error> {
let mut paths = vec!["/usr/local/include/libquil", "/usr/include/libquil"];
let mut paths = vec![
"/usr/local/include/libquil".to_string(),
"/usr/include/libquil".to_string(),
];

let libquil_src_path: Option<&'static str> = option_env!("LIBQUIL_SRC_PATH");
if let Some(libquil_src_path) = libquil_src_path {
if let Some(libquil_src_path) = env_path("LIBQUIL_SRC_PATH") {
paths.insert(0, libquil_src_path);
}

let c_include_path: Option<&'static str> = option_env!("C_INCLUDE_PATH");
if let Some(c_include_path) = c_include_path {
if let Some(c_include_path) = env_path("C_INCLUDE_PATH") {
paths.insert(0, c_include_path);
}

Expand All @@ -35,34 +55,68 @@ fn get_header_path() -> Result<PathBuf, Error> {
fn get_lib_search_paths() -> Vec<String> {
let mut paths = vec!["/usr/local/lib".to_string(), "/usr/lib".to_string()];

let libquil_src_path: Option<&'static str> = option_env!("LIBQUIL_SRC_PATH");
if let Some(libquil_src_path) = libquil_src_path {
// For installs that do not use /usr/local, where the headers and libraries live
// in separate directories and LIBQUIL_SRC_PATH names only the former.
if let Some(libquil_lib_path) = env_path("LIBQUIL_LIB_PATH") {
paths.insert(0, libquil_lib_path);
}

if let Some(libquil_src_path) = env_path("LIBQUIL_SRC_PATH") {
// libquil is a FASL library loaded into the libsbcl_librarian runtime, so
// both must be found. A source tree keeps the runtime in a subdirectory;
// an installed layout puts everything in one directory.
paths.insert(0, format!("{libquil_src_path}/runtime"));
paths.insert(0, libquil_src_path.to_string());
}

paths
}

fn main() -> Result<(), Error> {
/// Directories to search for headers. `libquil.h` includes `sbcl_librarian_err.h`,
/// and `get_error_message` is declared in `sbcl_librarian.h`, both of which ship
/// with the runtime.
fn get_include_paths(libquil_header_path: &std::path::Path) -> Vec<PathBuf> {
let mut paths = Vec::new();
if let Some(dir) = libquil_header_path.parent() {
paths.push(dir.to_path_buf());
paths.push(dir.join("runtime"));
}
paths.retain(|p| p.exists());
paths
}

fn main() {
// Cargo prints a build script's error with Debug, which would hide the
// explanation these errors carry, so report it and exit rather than returning it.
if let Err(error) = build() {
eprintln!("\nerror: {error}\n");
std::process::exit(1);
}
}

fn build() -> Result<(), Error> {
for envvar in PATH_ENVVARS {
println!("cargo:rerun-if-env-changed={envvar}");
}

let libquil_header_path = get_header_path()?;

for path in get_lib_search_paths() {
println!("cargo:rustc-link-search={}", path);
}

println!("cargo:rustc-link-lib=quil");
// The runtime that hosts libquil: it supplies the Lisp image, the error API
// (get_error_message) and the handle API (lisp_release_handle).
println!("cargo:rustc-link-lib=sbcl_librarian");

// Tell cargo to rerun if the libquil implementation has changed
println!(
"cargo:rustc-rerun-if-changed={}",
"cargo:rerun-if-changed={}",
libquil_header_path.clone().display()
);

// If this isn't set on MacOS, memory allocation errors occur when trying to initialize the
// library
if cfg!(target_os = "macos") {
println!("cargo:rustc-link-arg=-pagezero_size 0x100000");
}
let include_paths = get_include_paths(&libquil_header_path);

// The bindgen::Builder is the main entry point
// to bindgen, and lets you build up options for
Expand All @@ -71,6 +125,23 @@ fn main() -> Result<(), Error> {
// The input header we would like to generate
// bindings for.
.header(libquil_header_path.to_string_lossy())
// ...and the runtime's header, which declares the error API that libquil's
// functions report through.
.header(
include_paths
.iter()
.map(|dir| dir.join("sbcl_librarian.h"))
.find(|path| path.exists())
.ok_or_else(|| {
Error::RuntimeHeaderNotFound(libquil_header_path.display().to_string())
})?
.to_string_lossy(),
)
.clang_args(
include_paths
.iter()
.map(|dir| format!("-I{}", dir.display())),
)
// Tell cargo to invalidate the built crate whenever any of the
// included header files changed.
.parse_callbacks(Box::new(bindgen::CargoCallbacks))
Expand Down
Loading
Loading