From 247ed3e0a96265542745775550ffce4ec377385f Mon Sep 17 00:00:00 2001 From: Soutaro Matsumoto Date: Thu, 2 Apr 2026 17:00:22 +0900 Subject: [PATCH] Better rust development workflow --- .github/workflows/rust.yml | 98 ++++++++++++- .gitignore | 4 + Gemfile.lock | 5 +- Rakefile | 204 +++++++++++++++++++++++++++ docs/rust.md | 96 +++++++++++++ rust/rbs_version | 1 + rust/ruby-rbs-sys/vendor/rbs/include | 1 - rust/ruby-rbs-sys/vendor/rbs/src | 1 - rust/ruby-rbs/vendor/rbs/config.yml | 1 - 9 files changed, 401 insertions(+), 10 deletions(-) create mode 100644 docs/rust.md create mode 100644 rust/rbs_version delete mode 120000 rust/ruby-rbs-sys/vendor/rbs/include delete mode 120000 rust/ruby-rbs-sys/vendor/rbs/src delete mode 120000 rust/ruby-rbs/vendor/rbs/config.yml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 47afd4d837..a0df092c28 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -24,6 +24,20 @@ jobs: os: [ubuntu-latest, macos-latest, windows-latest] steps: - uses: actions/checkout@v6 + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler: none + - name: Update rubygems & bundler + run: gem update --system + - name: Install gems + run: | + bundle config set --local without libs:profilers + bundle install --jobs 4 --retry 3 + - name: Set up vendored RBS source + run: bundle exec rake rust:rbs:sync - name: Install Rust tools run: | rustup update --no-self-update stable @@ -42,12 +56,30 @@ jobs: cd rust cargo test --verbose - publish-dry-run: - name: cargo:publish-dry-run + publish-dry-run-ruby-rbs-sys: + name: rust:publish:ruby-rbs-sys runs-on: ubuntu-latest continue-on-error: true steps: - uses: actions/checkout@v6 + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Set up git identity + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler: none + - name: Update rubygems & bundler + run: gem update --system + - name: Install gems + run: | + bundle config set --local without libs:profilers + bundle install --jobs 4 --retry 3 + - name: Set up vendored RBS source + run: bundle exec rake rust:rbs:sync - name: Install Rust tools run: | rustup update --no-self-update stable @@ -61,16 +93,72 @@ jobs: key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} restore-keys: | ${{ runner.os }}-cargo- - - name: Test publish crates + - name: Test publish ruby-rbs-sys + run: bundle exec rake rust:publish:ruby-rbs-sys + env: + RBS_RUST_PUBLISH_DRY_RUN: "1" + + publish-dry-run-ruby-rbs: + name: rust:publish:ruby-rbs + runs-on: ubuntu-latest + continue-on-error: true + steps: + - uses: actions/checkout@v6 + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Set up git identity run: | - cd rust - cargo publish --dry-run + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler: none + - name: Update rubygems & bundler + run: gem update --system + - name: Install gems + run: | + bundle config set --local without libs:profilers + bundle install --jobs 4 --retry 3 + - name: Set up vendored RBS source + run: bundle exec rake rust:rbs:sync + - name: Install Rust tools + run: | + rustup update --no-self-update stable + rustup default stable + - uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rust/target + key: ${{ runner.os }}-cargo-${{ hashFiles('rust/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo- + - name: Test publish ruby-rbs + run: bundle exec rake rust:publish:ruby-rbs + env: + RBS_RUST_PUBLISH_DRY_RUN: "1" lint: name: cargo:lint runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + - run: git fetch --depth=1 origin +refs/tags/*:refs/tags/* + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: ruby + bundler: none + - name: Update rubygems & bundler + run: gem update --system + - name: Install gems + run: | + bundle config set --local without libs:profilers + bundle install --jobs 4 --retry 3 + - name: Set up vendored RBS source + run: bundle exec rake rust:rbs:sync - name: Install Rust tools run: | rustup update --no-self-update stable diff --git a/.gitignore b/.gitignore index 23c663ad8b..3d723e6d4b 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ doc/ # For clangd's editor integration ext/rbs_extension/compile_commands.json ext/rbs_extension/.cache + +# Rust crate vendored RBS source (managed by rake rust:rbs:sync or rust:rbs:symlink) +rust/ruby-rbs-sys/vendor/rbs/ +rust/ruby-rbs/vendor/rbs/ diff --git a/Gemfile.lock b/Gemfile.lock index 5b5ddc4d89..b87447ad1e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -116,8 +116,9 @@ GEM psych (4.0.6) stringio public_suffix (7.0.5) - raap (1.3.0) - rbs (~> 3.9.0) + raap (2.0.0) + logger + rbs (~> 4.0) timeout (~> 0.4) racc (1.8.1) rainbow (3.1.1) diff --git a/Rakefile b/Rakefile index 3388d609a4..68d1b54328 100644 --- a/Rakefile +++ b/Rakefile @@ -548,3 +548,207 @@ task :prepare_profiling do Rake::Task[:"templates"].invoke Rake::Task[:"compile"].invoke end + +namespace :rust do + namespace :rbs do + RUST_DIR = File.expand_path("rust", __dir__) + RBS_VERSION_FILE = File.join(RUST_DIR, "rbs_version") + + VENDOR_TARGETS = { + "ruby-rbs-sys" => %w[include src], + "ruby-rbs" => %w[config.yml], + } + + desc "Sync vendored RBS source from the pinned version" + task :sync do + unless File.exist?(RBS_VERSION_FILE) + raise "#{RBS_VERSION_FILE} not found. Run `rake rust:rbs:pin[VERSION]` first." + end + + version = File.read(RBS_VERSION_FILE).strip + raise "#{RBS_VERSION_FILE} is empty" if version.empty? + + puts "Syncing vendor/rbs/ from #{version}..." + + VENDOR_TARGETS.each do |crate, entries| + vendor_dir = File.join(RUST_DIR, crate, "vendor", "rbs") + + puts " Copying files for #{crate}:" + chmod_R "u+w", vendor_dir, verbose: false if File.exist?(vendor_dir) + rm_rf vendor_dir, verbose: false + mkdir_p vendor_dir, verbose: false + + entries.each do |entry| + target = File.join(vendor_dir, entry) + + # Extract the entry from the pinned git tag using git archive + IO.popen(["git", "archive", "--format=tar", version, "--", entry], "rb") do |tar| + IO.popen(["tar", "xf", "-", "-C", vendor_dir], "wb") do |extract| + IO.copy_stream(tar, extract) + end + end + + raise "Failed to extract #{entry} from #{version}" unless File.exist?(target) + puts " #{entry}" + end + + # Make files read-only to prevent accidental edits + chmod_R "a-w", vendor_dir, verbose: false + end + + puts "📦 Synced vendor/rbs/ from #{version} (read-only)" + end + + desc "Pin a specific RBS version for Rust crates (e.g., rake rust:rbs:pin[v4.0.3])" + task :pin, [:version] do |_t, args| + version = args[:version] or raise "Usage: rake rust:rbs:pin[VERSION]" + + # Verify the tag exists + unless system("git", "rev-parse", "--verify", "#{version}^{commit}", out: File::NULL, err: File::NULL) + raise "Tag #{version} not found" + end + + File.write(RBS_VERSION_FILE, "#{version}\n") + puts "📌 Pinned RBS version to #{version}" + + Rake::Task["rust:rbs:sync"].invoke + end + + desc "Create symlinks from vendor/rbs/ to the repository root (for development/CI)" + task :symlink do + VENDOR_TARGETS.each do |crate, entries| + vendor_dir = File.join(RUST_DIR, crate, "vendor", "rbs") + + puts "Setting up symlinks for #{crate}..." + entries.each do |entry| + puts " #{entry} -> repository root" + end + + chmod_R "u+w", vendor_dir, verbose: false if File.exist?(vendor_dir) + rm_rf vendor_dir, verbose: false + mkdir_p vendor_dir, verbose: false + + entries.each do |entry| + ln_s File.join("..", "..", "..", "..", entry), File.join(vendor_dir, entry), verbose: false + end + end + + puts "🔗 Symlinked vendor/rbs/ to repository root" + end + end + + namespace :publish do + def self.prepare_publish_branch(crate_name) + dry_run = ENV["RBS_RUST_PUBLISH_DRY_RUN"] + + version_file = File.join(RUST_DIR, "rbs_version") + + unless File.exist?(version_file) + raise "#{version_file} not found. Run `rake rust:rbs:pin[VERSION]` first." + end + + rbs_version = File.read(version_file).strip + raise "#{version_file} is empty" if rbs_version.empty? + + crate_version = File.read(File.join(RUST_DIR, crate_name, "Cargo.toml"))[/^version\s*=\s*"(.+)"/, 1] + release_branch = "rust/release-#{crate_name}-#{Time.now.strftime('%Y%m%d%H%M%S')}" + + puts "=" * 60 + puts "Rust crate publish: #{crate_name}#{dry_run ? " (DRY RUN)" : ""}" + puts "=" * 60 + puts " RBS source version: #{rbs_version}" + puts " #{crate_name}: #{crate_version} (tag: #{crate_name}-v#{crate_version})" + puts " Release branch: #{release_branch}" + puts "=" * 60 + + # Check that vendor dirs contain real files, not symlinks + entries = VENDOR_TARGETS.fetch(crate_name) + entries.each do |entry| + path = File.join(RUST_DIR, crate_name, "vendor", "rbs", entry) + if File.symlink?(path) + raise "#{path} is a symlink. Run `rake rust:rbs:sync` first." + end + unless File.exist?(path) + raise "#{path} does not exist. Run `rake rust:rbs:sync` first." + end + end + + # Ensure working tree is clean before publishing + unless `git status --porcelain`.strip.empty? + raise "💢 Working tree is dirty. Please commit or stash your changes before publishing." + end + + # Create a release branch with vendor files committed + original_branch = `git rev-parse --abbrev-ref HEAD`.strip + + sh "git", "checkout", "-b", release_branch, verbose: false + vendor_path = File.join("rust", crate_name, "vendor", "rbs") + sh "git", "add", "-f", vendor_path, verbose: false + sh "git", "commit", "-m", "Publish #{crate_name} (RBS #{rbs_version})", verbose: false + + [dry_run, crate_version, original_branch] + end + + desc "Publish ruby-rbs-sys crate to crates.io (set RBS_RUST_PUBLISH_DRY_RUN=1 for dry-run only)" + task :"ruby-rbs-sys" do + crate_name = "ruby-rbs-sys" + dry_run, crate_version, original_branch = prepare_publish_branch(crate_name) + + begin + puts "🔰 Dry-run publishing..." + + Dir.chdir(File.join(RUST_DIR, crate_name)) do + sh "cargo", "publish", "--dry-run" + end + + puts "✅ Dry-run succeeded!" + + unless dry_run + puts "💪 Publishing #{crate_name} for real..." + + Dir.chdir(File.join(RUST_DIR, crate_name)) do + sh "cargo", "publish" + end + + sh "git", "tag", "#{crate_name}-v#{crate_version}" + sh "git", "push", "origin", "#{crate_name}-v#{crate_version}" + + puts "🎉 Published #{crate_name} successfully!" + end + ensure + sh "git", "checkout", original_branch, verbose: false + end + end + + desc "Publish ruby-rbs crate to crates.io (set RBS_RUST_PUBLISH_DRY_RUN=1 for dry-run only)" + task :"ruby-rbs" do + crate_name = "ruby-rbs" + dry_run, crate_version, original_branch = prepare_publish_branch(crate_name) + + begin + puts "🔰 Dry-run publishing..." + + Dir.chdir(File.join(RUST_DIR, crate_name)) do + sh "cargo", "publish", "--dry-run", "--no-verify" + end + + puts "✅ Dry-run succeeded!" + + unless dry_run + puts "💪 Publishing #{crate_name} for real..." + + Dir.chdir(File.join(RUST_DIR, crate_name)) do + sh "cargo", "publish" + end + + sh "git", "tag", "#{crate_name}-v#{crate_version}" + sh "git", "push", "origin", "#{crate_name}-v#{crate_version}" + + puts "🎉 Published #{crate_name} successfully!" + end + ensure + sh "git", "checkout", original_branch, verbose: false + end + end + end +end diff --git a/docs/rust.md b/docs/rust.md new file mode 100644 index 0000000000..da0160c4e3 --- /dev/null +++ b/docs/rust.md @@ -0,0 +1,96 @@ +# Rust Crates + +RBS provides two Rust crates: + +- **`ruby-rbs-sys`** -- Low-level FFI bindings to the RBS C parser +- **`ruby-rbs`** -- High-level safe Rust API for parsing RBS signatures + +Both crates are published to [crates.io](https://crates.io/) and are developed within the `rust/` directory of this repository. + +## Vendored RBS Source + +The Rust crates depend on the RBS C parser source code (`include/`, `src/`) and configuration (`config.yml`) from this repository. These files are vendored into each crate's `vendor/rbs/` directory, which is managed by Rake tasks and not tracked by git. + +The file `rust/rbs_version` records which version of RBS the Rust crates are pinned to. + +## Setup + +After cloning the repository, set up the vendored source before building the Rust crates: + +```bash +rake rust:rbs:sync # Uses the pinned version from rust/rbs_version +``` + +Then build and test: + +```bash +cd rust +cargo test +``` + +## Rake Tasks + +### `rake rust:rbs:sync` + +Copies the source files from the pinned version into each crate's `vendor/rbs/`. The copied files are made read-only to prevent accidental edits. + +### `rake rust:rbs:pin[VERSION]` + +Records a git tag in `rust/rbs_version`. For example: + +```bash +rake rust:rbs:pin[v4.0.3] +``` + +### `rake rust:publish:ruby-rbs-sys` / `rake rust:publish:ruby-rbs` + +Publishes each crate to crates.io individually. Each task: + +1. Verifies `rust/rbs_version` is set +2. Verifies vendor directories contain real files (not symlinks) +3. Verifies the git working tree is clean +4. Creates a release branch and commits the vendor files +5. Runs a dry-run to check packaging +6. Publishes the crate + +Set `RBS_RUST_PUBLISH_DRY_RUN=1` to only run the dry-run step and skip the actual publish to crates.io. This is used in CI to verify that the crates can be packaged correctly. + +### `rake rust:rbs:symlink` + +If your development needs unreleased version of RBS source code, use `rake rust:rbs:symlink` to set up symlinks in vendor directories to refer the worktree source code. Changes to the C parser source are immediately reflected in Rust builds. + +## Publishing Workflow + +1. Pin the RBS version to release against: + + ```bash + rake rust:rbs:pin[v4.0.3] + ``` + +2. Sync the vendored source: + + ```bash + rake rust:rbs:sync + ``` + +3. Update crate versions in `rust/ruby-rbs-sys/Cargo.toml` and `rust/ruby-rbs/Cargo.toml`. + +4. Build and test: + + ```bash + cd rust && cargo test + ``` + +5. Commit the version changes and `rust/rbs_version`: + + ```bash + git add rust/rbs_version rust/ruby-rbs-sys/Cargo.toml rust/ruby-rbs/Cargo.toml + git commit -m "Bump Rust crate versions" + ``` + +6. Publish each crate: + + ```bash + rake rust:publish:ruby-rbs-sys + rake rust:publish:ruby-rbs + ``` diff --git a/rust/rbs_version b/rust/rbs_version new file mode 100644 index 0000000000..bda368d509 --- /dev/null +++ b/rust/rbs_version @@ -0,0 +1 @@ +v4.0.2 diff --git a/rust/ruby-rbs-sys/vendor/rbs/include b/rust/ruby-rbs-sys/vendor/rbs/include deleted file mode 120000 index 0f0436a651..0000000000 --- a/rust/ruby-rbs-sys/vendor/rbs/include +++ /dev/null @@ -1 +0,0 @@ -../../../../include \ No newline at end of file diff --git a/rust/ruby-rbs-sys/vendor/rbs/src b/rust/ruby-rbs-sys/vendor/rbs/src deleted file mode 120000 index b3e266f122..0000000000 --- a/rust/ruby-rbs-sys/vendor/rbs/src +++ /dev/null @@ -1 +0,0 @@ -../../../../src \ No newline at end of file diff --git a/rust/ruby-rbs/vendor/rbs/config.yml b/rust/ruby-rbs/vendor/rbs/config.yml deleted file mode 120000 index 0ecb7253d4..0000000000 --- a/rust/ruby-rbs/vendor/rbs/config.yml +++ /dev/null @@ -1 +0,0 @@ -../../../../config.yml \ No newline at end of file