Skip to content
Merged
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
21 changes: 21 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,27 @@ jobs:
- name: Checkout raysense
uses: actions/checkout@v6

- name: Vendor rayforce at pinned SHA
# The published .crate must contain the rayforce source so end users
# can `cargo install raysense` without network access. We clone it
# here, strip .git/, and let `cargo package` pick it up via the
# Cargo.toml `include` whitelist (vendor/ is gitignored otherwise).
shell: bash
run: |
set -euo pipefail
sha="$(cat .rayforce-version | tr -d '[:space:]')"
if [[ -z "$sha" ]]; then
echo "::error::.rayforce-version is empty" >&2
exit 1
fi
mkdir -p vendor
rm -rf vendor/rayforce
git -c advice.detachedHead=false clone --quiet \
https://github.com/RayforceDB/rayforce.git vendor/rayforce
git -C vendor/rayforce checkout --quiet "$sha"
rm -rf vendor/rayforce/.git
test -f vendor/rayforce/Makefile || { echo "::error::Makefile missing" >&2; exit 1; }

- name: Test
run: cargo test

Expand Down
23 changes: 2 additions & 21 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,26 +1,7 @@
# Copyright (c) 2025-2026 Anton Kundenko <singaraiona@gmail.com>
# All rights reserved.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

CLAUDE.md
/target/
Cargo.lock
/local/
.codex
/deps/rayforce/
/vendor/
1 change: 1 addition & 0 deletions .rayforce-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
6614c1baebf25f0fd0db02cb0de214a92e22f550
21 changes: 19 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,24 @@ repository = "https://github.com/RayforceDB/raysense"
description = "Architectural X-ray for your codebase. Live, local, agent-ready."
readme = "README.md"
links = "rayforce"
# Whitelist files shipped in the published .crate. `vendor/rayforce/**` is
# included even though it's gitignored — CI populates it from upstream
# rayforce at the pinned SHA before `cargo package` runs.
include = [
"src/**/*.rs",
"build.rs",
"Cargo.toml",
"README.md",
".rayforce-version",
# Bundled rayforce source — narrow to just what `make lib` needs.
# Excludes website/, docs/, test/, examples/, bench/ to keep the
# published .crate well under the crates.io 10 MiB ceiling.
"vendor/rayforce/Makefile",
"vendor/rayforce/LICENSE",
"vendor/rayforce/include/**/*.h",
"vendor/rayforce/src/**/*.c",
"vendor/rayforce/src/**/*.h",
]

[[bin]]
name = "raysense"
Expand Down Expand Up @@ -56,5 +74,4 @@ tree-sitter-python = "0.25.0"
tree-sitter-rust = "0.24.2"
tree-sitter-typescript = "0.23.2"

[build-dependencies]
cc = "1"
# build.rs uses only std + git/make subprocesses; no build-deps.
218 changes: 148 additions & 70 deletions build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,56 @@
* SOFTWARE.
*/

//! Compile the vendored C library directly via `cc`. No external checkout
//! required — `cargo build` works from a fresh clone with no extra steps.
//! Set `RAYFORCE_DIR` only if you want to link against an outside build for
//! development.
//! Build raysense by linking the upstream rayforce static library.
//!
//! Three resolution modes, in priority order:
//!
//! 1. `RAYFORCE_DIR` env var — link an externally built `librayforce.a`
//! from a developer-provided rayforce checkout (you build rayforce
//! yourself, point raysense at it).
//!
//! 2. `vendor/rayforce/Makefile` exists in the source tree (bundled
//! inside the published `.crate` tarball, or populated by CI before
//! `cargo package`) — copy that source into `OUT_DIR` and build it
//! there.
//!
//! 3. Otherwise — clone upstream rayforce at the SHA pinned in
//! `.rayforce-version` directly into `OUT_DIR`, then build it there.
//!
//! All `make lib` work happens inside `OUT_DIR/rayforce-build/`. The
//! source tree is never modified — required by `cargo package`'s
//! verification step (build scripts must not write outside `OUT_DIR`).
//!
//! The Makefile's stock `RELEASE_CFLAGS` includes `-march=native`, which
//! bakes the build host's CPU features into the static library and would
//! crash on older CPUs of the same arch. We override `RELEASE_CFLAGS` to
//! a portable baseline so the produced `.a` is shippable across hosts.

use std::env;
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

const RAYFORCE_REPO: &str = "https://github.com/RayforceDB/rayforce.git";

/// Portable release CFLAGS. Differs from upstream `RELEASE_CFLAGS` by
/// dropping `-march=native` (build-host-specific) and `-Werror` (would
/// fail downstream builds on new compiler warnings).
const PORTABLE_CFLAGS: &str = "-fPIC -O3 -fomit-frame-pointer -fno-math-errno \
-funroll-loops -std=c17 -Wall -Wextra -Wstrict-prototypes \
-Wno-unused-parameter";

fn main() {
let manifest_dir = PathBuf::from(env::var("CARGO_MANIFEST_DIR").unwrap());

if let Some(external_dir) = env::var_os("RAYFORCE_DIR") {
link_external(PathBuf::from(external_dir));
if let Some(external) = env::var_os("RAYFORCE_DIR") {
link_external(PathBuf::from(external));
} else {
compile_vendored(&manifest_dir.join("vendor/rayforce"));
let out_dir = PathBuf::from(env::var("OUT_DIR").unwrap());
let build_dir = out_dir.join("rayforce-build");
ensure_build_dir(&manifest_dir, &build_dir);
run_make_lib(&build_dir);
link_static_lib(&build_dir);
}

if env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("linux") {
Expand All @@ -46,59 +81,124 @@ fn main() {
}

println!("cargo:rerun-if-env-changed=RAYFORCE_DIR");
println!("cargo:rerun-if-changed=.rayforce-version");
}

/// Default path: build the vendored sources with `cc::Build`. Excludes the
/// REPL binary entry (`src/app/main.c`) since we only need the library.
fn compile_vendored(vendor_dir: &Path) {
let include_dir = vendor_dir.join("include");
let src_dir = vendor_dir.join("src");
let mut build = cc::Build::new();
build
.std("c17")
.include(&include_dir)
.include(&src_dir)
.flag_if_supported("-fPIC")
.flag_if_supported("-Wno-unused-parameter")
.flag_if_supported("-Wno-unused-but-set-variable")
.flag_if_supported("-Wno-unused-variable")
.flag_if_supported("-Wno-unused-function");

if let Ok(profile) = env::var("PROFILE") {
if profile == "release" {
build
.opt_level(3)
.flag_if_supported("-funroll-loops")
.flag_if_supported("-fomit-frame-pointer")
.flag_if_supported("-fno-math-errno");
/// Materialize rayforce source under `build_dir`. Either copy bundled
/// `vendor/rayforce/` from the source tree, or clone upstream at the
/// pinned SHA. Skips work if `build_dir` already holds the right SHA.
fn ensure_build_dir(manifest_dir: &Path, build_dir: &Path) {
let pinned_sha = read_pin(manifest_dir);
let sentinel = build_dir.join(".raysense-built-sha");

if let Ok(prev) = fs::read_to_string(&sentinel) {
if prev.trim() == pinned_sha {
return;
}
}

let mut count = 0usize;
for entry in walk_c_sources(&src_dir) {
if entry.ends_with(Path::new("app/main.c"))
|| entry.ends_with(Path::new("app/repl.c"))
|| entry.ends_with(Path::new("app/term.c"))
{
continue;
}
println!("cargo:rerun-if-changed={}", entry.display());
build.file(&entry);
count += 1;
if build_dir.exists() {
fs::remove_dir_all(build_dir).expect("rm previous rayforce-build/");
}

let bundled = manifest_dir.join("vendor/rayforce");
if bundled.join("Makefile").exists() {
copy_tree(&bundled, build_dir);
} else {
clone_at_pin(build_dir, &pinned_sha);
}

fs::write(&sentinel, &pinned_sha).expect("write sentinel");
}

fn read_pin(manifest_dir: &Path) -> String {
let path = manifest_dir.join(".rayforce-version");
let raw = fs::read_to_string(&path).unwrap_or_else(|e| panic!("read {}: {e}", path.display()));
let sha = raw.trim();
if sha.len() < 7 || !sha.chars().all(|c| c.is_ascii_hexdigit()) {
panic!("`.rayforce-version` does not contain a hex SHA: {sha:?}");
}
sha.to_string()
}

fn copy_tree(src: &Path, dst: &Path) {
fs::create_dir_all(dst).expect("mkdir build_dir");
// `cp -r` is robust on Linux/macOS; the upstream Makefile only
// supports those platforms anyway. Trailing dot copies contents,
// not the src dir itself.
let status = Command::new("cp")
.arg("-R")
.arg(format!("{}/.", src.display()))
.arg(dst)
.status()
.unwrap_or_else(|e| panic!("`cp -R {src:?} -> {dst:?}` failed: {e}"));
if !status.success() {
panic!("`cp -R` exited {status}");
}
}

fn clone_at_pin(build_dir: &Path, sha: &str) {
if let Some(parent) = build_dir.parent() {
fs::create_dir_all(parent).expect("mkdir build_dir parent");
}
if count == 0 {
fs::create_dir_all(build_dir).expect("mkdir build_dir");

run_git(build_dir, &["init", "-q"]);
run_git(build_dir, &["remote", "add", "origin", RAYFORCE_REPO]);
run_git(build_dir, &["fetch", "--depth", "1", "origin", sha]);
run_git(build_dir, &["checkout", "--quiet", "FETCH_HEAD"]);
// Strip .git/ — keeps OUT_DIR small and prevents stale clone state
// from confusing future cache hits.
let dot_git = build_dir.join(".git");
if dot_git.exists() {
let _ = fs::remove_dir_all(&dot_git);
}
}

fn run_git(cwd: &Path, args: &[&str]) {
let status = Command::new("git")
.current_dir(cwd)
.args(args)
.status()
.unwrap_or_else(|e| panic!("failed to run `git {}`: {e}", args.join(" ")));
if !status.success() {
panic!("`git {}` failed with status {}", args.join(" "), status);
}
}

fn run_make_lib(build_dir: &Path) {
let status = Command::new("make")
.current_dir(build_dir)
.arg("lib")
.arg(format!("RELEASE_CFLAGS={PORTABLE_CFLAGS}"))
.status()
.unwrap_or_else(|e| panic!("failed to run `make lib`: {e}"));
if !status.success() {
panic!(
"no C sources found under {} — vendor/ is empty?",
src_dir.display()
"`make lib` in {} exited with status {}",
build_dir.display(),
status
);
}
println!("cargo:rerun-if-changed={}", include_dir.display());
let lib = build_dir.join("librayforce.a");
if !lib.exists() {
panic!(
"expected {} after `make lib`, but it is missing",
lib.display()
);
}
println!("cargo:rerun-if-changed={}", lib.display());
}

fn link_static_lib(build_dir: &Path) {
let include_dir = build_dir.join("include");
println!("cargo:include={}", include_dir.display());
build.compile("rayforce");
println!("cargo:rustc-link-search=native={}", build_dir.display());
println!("cargo:rustc-link-lib=static=rayforce");
}

/// Optional: link against an externally-built `librayforce.a`. Used only for
/// rayforce development; everyone else gets the vendored compile path above.
/// Optional: link against an externally-built `librayforce.a`. Used only
/// for rayforce development; everyone else gets the auto-vendored path.
fn link_external(rayforce_dir: PathBuf) {
let include_dir = rayforce_dir.join("include");
let lib_path = rayforce_dir.join("librayforce.a");
Expand All @@ -119,25 +219,3 @@ fn link_external(rayforce_dir: PathBuf) {
include_dir.join("rayforce.h").display()
);
}

/// Walk a directory tree collecting all `*.c` files. Pure-std (no walkdir
/// dep) to keep build-deps minimal.
fn walk_c_sources(root: &Path) -> Vec<PathBuf> {
let mut out = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
stack.push(path);
} else if path.extension().and_then(|s| s.to_str()) == Some("c") {
out.push(path);
}
}
}
out.sort();
out
}
21 changes: 0 additions & 21 deletions vendor/rayforce/LICENSE

This file was deleted.

Loading