From 52b9de24b3ec125dbd6a0ed346a171f13e61db3a Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 8 Nov 2026 07:56:01 +0000 Subject: [PATCH 1/4] Document Alef Rakefile ownership handoff --- gems/kettle-jem/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/gems/kettle-jem/README.md b/gems/kettle-jem/README.md index d7e44494a..72f31c1a4 100644 --- a/gems/kettle-jem/README.md +++ b/gems/kettle-jem/README.md @@ -550,12 +550,37 @@ files: Rakefile: strategy: merge preference: destination # keep your custom tasks + add_template_only_nodes: true certs/my.pem: strategy: raw_copy # binary file, no merging generated/report.md: strategy: keep_destination # never touch this file ``` +### Alef-generated Ruby packages + +For an Alef-generated monorepo subgem, generate the package first, then hand +the Rakefile over to Kettle Jem by adding its repository-relative path to +`[workspace.ownership].user_owned` in `alef.toml`. Keep any existing entries: + +```toml +[workspace.ownership] +user_owned = [ + "packages/ruby/Rakefile", + "packages/ruby/structuredmerge_core.gemspec", +] +``` + +Use the Rakefile override above in the package's Kettle Jem configuration. +The monorepo subgem profile seeds `accept_template` for a new configuration; +an explicit `merge` strategy survives subsequent profile synchronization. +Destination preference retains native build tasks and `rb_sys` configuration, +while template-only nodes add Kettle's tasks. Once handed off, Alef no longer +regenerates or verifies that file. Run `alef verify --exit-code` after the +handoff, and rerun Alef generation and Kettle templating to check convergence. +Configure the package as a member of `kettle-family` for its local-template +and release workflows. + ### Environment Variables & CLI Options Kettle::Jem behavior is controlled via environment variables (which double as From 8ccdf28e9e740a5874d4e0d1c3e5ca2d7193fdda Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 8 Nov 2026 07:58:22 +0000 Subject: [PATCH 2/4] Exercise Alef native Rakefile overlay --- gems/kettle-jem/spec/fixtures/alef/Rakefile | 95 +++++++++++++++++++ .../structural_merge_template_spec.rb | 32 +++++++ 2 files changed, 127 insertions(+) create mode 100644 gems/kettle-jem/spec/fixtures/alef/Rakefile diff --git a/gems/kettle-jem/spec/fixtures/alef/Rakefile b/gems/kettle-jem/spec/fixtures/alef/Rakefile new file mode 100644 index 000000000..8e128c36a --- /dev/null +++ b/gems/kettle-jem/spec/fixtures/alef/Rakefile @@ -0,0 +1,95 @@ +# This file is auto-generated by alef — DO NOT EDIT. +# alef:hash:53a4b3692134151096d1676cc69dbb7778ef8df86acdf81b887282dc08d085b2 +# To regenerate: alef generate +# To verify freshness: alef verify + +# frozen_string_literal: true + +require "bundler" +Bundler::GemHelper.install_tasks name: "structuredmerge_core" +require "rb_sys/extensiontask" +require "rspec/core/rake_task" + +# Absolute path to the gem package directory, used as the anchor for resolving +# the gemspec and the native extension's Cargo manifest. +GEM_ROOT = __dir__ +# Loaded gemspec used by Rake::ExtensionTask to compile the native extension. +GEMSPEC = Gem::Specification.load(File.expand_path("structuredmerge_core.gemspec", GEM_ROOT)) + +# Set of supported platform identifiers for native gem cross-compilation. +# Used by `rb_sys/extensiontask` to drive the `rake compile:` tasks +# that produce platform-specific prebuilt gems published alongside the source +# gem on RubyGems. +CROSS_PLATFORMS = %w[ + x86_64-linux + aarch64-linux + arm64-darwin + x86_64-darwin + x64-mingw-ucrt +].freeze + +# rb_sys 0.9.x's Cargo::Metadata runs `cargo metadata` without `--manifest-path`, +# so it resolves to whatever workspace contains cwd. In this monorepo the root +# workspace excludes our crate, so the lookup fails with PackageNotFoundError. +# Chdir-around-construction also doesn't work because Rake::ExtensionTask resolves +# its own paths (lib_dir, ext_dir, task wiring) at construction time relative to +# cwd, breaking the compile pipeline. Patch Cargo::Metadata#cargo_metadata to add +# the explicit `--manifest-path` pointing at the crate's Cargo.toml so the lookup +# is unambiguous regardless of cwd. +MANIFEST_PATH = File.expand_path("ext/structuredmerge_core_rb/native/Cargo.toml", GEM_ROOT) + +# @!visibility private +module RbSys + # @!visibility private + module Cargo + # @!visibility private + class Metadata + manifest_path = MANIFEST_PATH + define_method(:cargo_metadata) do + return @cargo_metadata if @cargo_metadata + + cargo = ENV["CARGO"] || "cargo" + args = ["metadata", "--format-version", "1", "--manifest-path", manifest_path] + args << "--no-deps" unless @deps + out, stderr, status = Open3.capture3(cargo, *args) + out.force_encoding(Encoding::UTF_8) + raise "exited with non-zero status (#{status})" unless status.success? + + data = JSON.parse(out) + raise "metadata must be a Hash" unless data.is_a?(Hash) + + @cargo_metadata = data + rescue StandardError => e + raise CargoMetadataError.new(e, stderr) + end + private :cargo_metadata + end + end +end + +RbSys::ExtensionTask.new("structuredmerge-core-rb", GEMSPEC) do |ext| + ext.lib_dir = "lib" + ext.ext_dir = "ext/structuredmerge_core_rb/native" + ext.source_pattern = "*.{}" + ext.platform = "ruby" + ext.cross_compile = true + ext.cross_platform = CROSS_PLATFORMS + # Pin cross_compile_versions to Ruby 3.2-3.5 stable releases. + # This overrides the container's RUBY_CC_VERSION env var at rake task definition time. + # The setter was added in a later rb_sys version; guard against older gem installations + # where the method does not exist (older rb_sys lines predating the 0.9.130 floor). + # rb-sys-dock 0.9.x ships images for Ruby 3.2, 3.3, 3.4, and 3.5; this list must + # match those available images. Per-ABI platform gem windows are controlled by rake-compiler-dock. + ext.cross_compile_versions = %w[3.5.0 3.4.9 3.3.11 3.2.11] if ext.respond_to?(:cross_compile_versions=) +end + +RSpec::Core::RakeTask.new(:spec) + +# rake-compiler's `compile` task is a no-op when cross_compile is true; the real +# work hangs off `compile:`. Wire `compile` → `compile:ruby` so +# both the dev shorthand and CI's +# `BUNDLE_PATH=vendor/bundle ruby -S bundle exec ruby -S rake compile` actually build. +task compile: "compile:ruby" + +task spec: :compile +task default: :spec diff --git a/gems/kettle-jem/spec/integration/structural_merge_template_spec.rb b/gems/kettle-jem/spec/integration/structural_merge_template_spec.rb index 3cc068e8d..37bcaa248 100644 --- a/gems/kettle-jem/spec/integration/structural_merge_template_spec.rb +++ b/gems/kettle-jem/spec/integration/structural_merge_template_spec.rb @@ -2334,6 +2334,38 @@ def call end end + it "converges when overlaying the packaged Rakefile on an Alef native package" do + Dir.mktmpdir("kettle-jem-alef-rakefile") do |root| + write_tree(root, { + "structuredmerge_core.gemspec" => 'Gem::Specification.new { |spec| spec.name = "structuredmerge-core" }', + ".kettle-jem.yml" => <<~YAML, + templates: + root: packaged + apply: true + profile: monorepo-subgem-release + entries: + - Rakefile + files: + Rakefile: + strategy: merge + preference: destination + add_template_only_nodes: true + YAML + "Rakefile" => File.read(File.expand_path("../fixtures/alef/Rakefile", __dir__)) + }) + + described_class.apply_project(root, env: {}, run_options: {skip_commit: true}) + first = File.read(File.join(root, "Rakefile")) + described_class.apply_project(root, env: {}, run_options: {skip_commit: true}) + second = File.read(File.join(root, "Rakefile")) + + expect(second).to eq(first) + expect(second).to include('require "rb_sys/extensiontask"', 'ext.cross_compile = true', 'ext.cross_platform = CROSS_PLATFORMS') + expect(second).to include('task compile: "compile:ruby"', 'require "kettle/dev"') + expect(second.scan("# This file is auto-generated by alef").length).to eq(1) + end + end + it "normalizes generated Rakefile section spacing after merge" do rakefile = described_class.send(:normalize_generated_rakefile, <<~RAKE) task :custom do From 37d88da6c2b597782e7012329ffee23ed76fbc1b Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 8 Nov 2026 08:02:33 +0000 Subject: [PATCH 3/4] Preserve merged Rakefile during scaffold cleanup --- gems/kettle-jem/lib/kettle/jem.rb | 5 ++++- .../spec/integration/structural_merge_template_spec.rb | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/gems/kettle-jem/lib/kettle/jem.rb b/gems/kettle-jem/lib/kettle/jem.rb index 49edd1ffb..3dd1cef51 100644 --- a/gems/kettle-jem/lib/kettle/jem.rb +++ b/gems/kettle-jem/lib/kettle/jem.rb @@ -3433,7 +3433,10 @@ def recipe_pack(facts) facts: %w[templates shim] ) end - unless shim_template_profile?(facts) + rakefile_template = recipes.any? do |recipe| + TEMPLATE_SOURCE_APPLICATION_RECIPE.match?(recipe.fetch(:name)) && recipe.fetch(:target_path) == "Rakefile" + end + unless shim_template_profile?(facts) || rakefile_template recipes << recipe_entry( "rakefile_scaffold_cleanup", "Rakefile", diff --git a/gems/kettle-jem/spec/integration/structural_merge_template_spec.rb b/gems/kettle-jem/spec/integration/structural_merge_template_spec.rb index 37bcaa248..5b6e52192 100644 --- a/gems/kettle-jem/spec/integration/structural_merge_template_spec.rb +++ b/gems/kettle-jem/spec/integration/structural_merge_template_spec.rb @@ -2340,9 +2340,8 @@ def call "structuredmerge_core.gemspec" => 'Gem::Specification.new { |spec| spec.name = "structuredmerge-core" }', ".kettle-jem.yml" => <<~YAML, templates: - root: packaged + root: template apply: true - profile: monorepo-subgem-release entries: - Rakefile files: @@ -2351,7 +2350,8 @@ def call preference: destination add_template_only_nodes: true YAML - "Rakefile" => File.read(File.expand_path("../fixtures/alef/Rakefile", __dir__)) + "Rakefile" => File.read(File.expand_path("../fixtures/alef/Rakefile", __dir__)), + "template/Rakefile.example" => File.read(File.expand_path("../../lib/kettle/jem/templates/Rakefile.example", __dir__)) }) described_class.apply_project(root, env: {}, run_options: {skip_commit: true}) From 60685d9ac6f6fe46dd54a1838a179ed9ed6c1317 Mon Sep 17 00:00:00 2001 From: "Peter H. Boling" Date: Sun, 8 Nov 2026 08:02:41 +0000 Subject: [PATCH 4/4] Explain Rakefile recipe ownership --- gems/kettle-jem/lib/kettle/jem.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/gems/kettle-jem/lib/kettle/jem.rb b/gems/kettle-jem/lib/kettle/jem.rb index 3dd1cef51..5e2c9a315 100644 --- a/gems/kettle-jem/lib/kettle/jem.rb +++ b/gems/kettle-jem/lib/kettle/jem.rb @@ -3433,6 +3433,9 @@ def recipe_pack(facts) facts: %w[templates shim] ) end + # Cleanup plans against the original file and would overwrite a merged + # Rakefile when both recipes change it. The template merge owns that + # file whenever it is selected; retain cleanup for standalone runs. rakefile_template = recipes.any? do |recipe| TEMPLATE_SOURCE_APPLICATION_RECIPE.match?(recipe.fetch(:name)) && recipe.fetch(:target_path) == "Rakefile" end