Skip to content

Add platform-specific gem dependencies - #44

Merged
eitoball merged 2 commits into
masterfrom
issue-31-platform-gems
Aug 1, 2026
Merged

Add platform-specific gem dependencies#44
eitoball merged 2 commits into
masterfrom
issue-31-platform-gems

Conversation

@eitoball

@eitoball eitoball commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Resolves #31.

Publishes separate Ruby, Linux, and mingw-ucrt gem artifacts so ICMP dependencies are selected by platform. Adds artifact metadata validation to CI and packaging tests.

Spec

Platform-specific gem dependencies

Goal

Resolve issue #31 by publishing platform-specific net-ping gems whose
runtime dependencies accurately describe the requirements for ICMP support.
The solution also corrects the equivalent Linux cap2 metadata omission.

Scope

The release produces three gems with the same name and version:

Gem platform Additional runtime dependency
ruby None
universal-linux cap2 (>= 0.2.2)
universal-mingw-ucrt win32-security (>= 0.2.0), win32ole (>= 1.8.8)

Only the current RubyInstaller mingw-ucrt platform is supported for Windows.
Legacy mingw32 is out of scope.

The CI workflow builds and validates these gems, but does not publish them.
The existing release process remains responsible for pushing all three files
to RubyGems.

Design

net-ping.gemspec becomes the OS-independent Ruby gemspec. It must not
inspect the host OS or add OS-specific runtime dependencies.

net-ping-universal-linux.gemspec loads the base specification, changes its
platform to universal-linux, and adds cap2. The platform is intentionally
CPU-independent so RubyGems can select it for supported Linux CPU variants.

net-ping-universal-mingw-ucrt.gemspec loads the same base specification,
changes its platform to universal-mingw-ucrt, and adds win32-security.

The Rake gem namespace explicitly enumerates the three gemspecs. gem:create
cleans old gem artifacts and builds all three. gem:install selects the
specific generated specification compatible with the local platform, falling
back to the Ruby specification when no platform-specific specification
matches, then installs its artifact.

Verification

Add gem:check, which reads each generated gem through RubyGems and fails
when its platform or runtime dependencies differ from this specification:

  • Ruby gem: platform ruby; neither cap2 nor win32-security.
  • Linux gem: platform universal-linux; includes cap2.
  • Windows gem: platform universal-mingw-ucrt; includes win32-security.

The existing GitHub Actions matrix continues to run the test suite. It also
runs gem:create and gem:check on both Ubuntu and Windows. A missing,
malformed, or incorrectly attributed dependency causes the job to fail.

Documentation

Update the prerequisite/install documentation to state that RubyGems chooses
the platform-specific package and installs the Linux or Windows ICMP
dependency automatically.

Add a CHANGES entry describing the corrected platform-specific dependency
metadata.

Non-goals

  • Publishing gems from CI.
  • Supporting legacy mingw32.
  • Changing ICMP runtime behavior.
  • Altering non-ICMP protocol implementations or tests.
Plan

Platform-specific Gem Dependencies Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: Build and validate RubyGems artifacts that install cap2 on Linux and win32-security on current Windows RubyInstaller systems.

Architecture: Keep net-ping.gemspec OS-independent and create two small gemspec overlays for Linux and mingw-ucrt. Centralize artifact generation and metadata validation in the Rake gem namespace, then run that validation in the existing Linux/Windows CI matrix.

Tech Stack: Ruby, RubyGems (Gem::Specification, Gem::Package, Gem::Platform), Rake, test-unit, GitHub Actions.

Global Constraints

  • Produce exactly three same-version net-ping gems: ruby, universal-linux, and universal-mingw-ucrt.
  • universal-linux must add cap2 (>= 0.2.2); universal-mingw-ucrt must add win32-security (>= 0.2.0).
  • The Ruby gem must not contain either OS-specific runtime dependency.
  • Support mingw-ucrt only; legacy mingw32 is out of scope.
  • CI builds and validates artifacts but never publishes them.
  • Do not create git commits for this work.

File Structure

File Responsibility
net-ping.gemspec Define the OS-independent Ruby artifact and common metadata.
net-ping-universal-linux.gemspec Overlay universal-linux and the cap2 runtime dependency.
net-ping-universal-mingw-ucrt.gemspec Overlay universal-mingw-ucrt and the win32-security runtime dependency.
Rakefile Build, inspect, and locally install the platform-appropriate artifact.
test/test_net_ping_gem_packaging.rb Build temporary artifacts and assert their platform/dependency metadata.
test/test_net_ping.rb Load the packaging test in the aggregate suite.
.github/workflows/test.yml Run gem artifact checks on Linux and Windows.
README.md Explain automatic platform-specific dependency installation.
CHANGES Record the metadata correction for the next release.

Task 1: Define and test platform gem specifications

Files:

  • Create: net-ping-universal-linux.gemspec
  • Create: net-ping-universal-mingw-ucrt.gemspec
  • Create: test/test_net_ping_gem_packaging.rb
  • Modify: net-ping.gemspec:1-40
  • Modify: test/test_net_ping.rb

Interfaces:

  • Consumes: net-ping.gemspec, which supplies the shared Gem::Specification.

  • Produces: three loadable gemspecs whose platform and runtime_dependencies describe their target platform.

  • Step 1: Write the failing package-metadata test

Create test/test_net_ping_gem_packaging.rb. Load every gemspec before building any artifact so spec.files cannot include an artifact created for an earlier specification. Build the loaded specifications into temporary .gem files, open each using Gem::Package, and assert the platform and runtime dependencies:

require 'rubygems/package'
require 'test/unit'

class TestNetPingGemPackaging < Test::Unit::TestCase
  EXPECTATIONS = {
    'net-ping.gemspec' => ['ruby', {}],
    'net-ping-universal-linux.gemspec' => ['universal-linux', {'cap2' => '>= 0.2.2'}],
    'net-ping-universal-mingw-ucrt.gemspec' => [
      'universal-mingw-ucrt',
      {'win32-security' => '>= 0.2.0'}
    ]
  }.freeze

  def setup
    @specifications = EXPECTATIONS.keys.map do |path|
      [path, Gem::Specification.load(path)]
    end
    @gem_files = @specifications.map { |_, spec| Gem::Package.build(spec) }
  end

  def teardown
    @gem_files.each { |path| File.delete(path) if File.exist?(path) }
  end

  def test_platforms_and_runtime_dependencies
    @specifications.each do |path, spec|
      expected_platform, expected_dependencies = EXPECTATIONS.fetch(path)
      packaged_spec = Gem::Package.new(spec.file_name).spec
      dependencies = packaged_spec.runtime_dependencies.to_h do |dependency|
        [dependency.name, dependency.requirement.to_s]
      end

      assert_equal(expected_platform, packaged_spec.platform.to_s, path)
      assert_equal(expected_dependencies, dependencies, path)
    end
  end
end

Add require 'test_net_ping_gem_packaging' to test/test_net_ping.rb.

  • Step 2: Run the new test to verify it fails

Run:

bundle exec ruby -Itest test/test_net_ping_gem_packaging.rb

Expected: failure because the Linux and Windows overlay gemspec files do not exist.

  • Step 3: Make the base gemspec OS-independent and add overlays

Remove the File::ALT_SEPARATOR, RbConfig, and RUBY_PLATFORM conditional runtime dependency logic from net-ping.gemspec; leave only shared metadata and development dependencies.

Create net-ping-universal-linux.gemspec:

spec = Gem::Specification.load('net-ping.gemspec')
spec.platform = Gem::Platform.new(['universal', 'linux'])
spec.add_dependency('cap2', '>= 0.2.2')
spec

Create net-ping-universal-mingw-ucrt.gemspec:

spec = Gem::Specification.load('net-ping.gemspec')
spec.platform = Gem::Platform.new(['universal', 'mingw-ucrt'])
spec.add_dependency('win32-security', '>= 0.2.0')
spec

Use the project’s existing block-style Gem::Specification.new format for the base gemspec. Verify the installed RubyGems version accepts the two platform strings with Gem::Platform.new.

  • Step 4: Run the package-metadata test to verify it passes

Run:

bundle exec ruby -Itest test/test_net_ping_gem_packaging.rb

Expected: PASS; all three artifacts have the expected platform and exactly the expected runtime dependencies.

  • Step 5: Run the aggregate test suite

Run:

bundle exec rake test

Expected: PASS.

Task 2: Make Rake build, validate, and install platform artifacts deterministically

Files:

  • Modify: Rakefile:6-29

Interfaces:

  • Consumes: the three gemspec paths from Task 1.

  • Produces: gem:create builds all artifacts; gem:check aborts on invalid artifact metadata; gem:install installs the local platform’s specific artifact or the Ruby fallback.

  • Step 1: Add a failing artifact-validation command

Add gem:check to the gem namespace. It must load the three expected gem files with Gem::Package, map each runtime dependency to name => requirement.to_s, and abort if the platform or dependency hash differs from:

{
  'ruby' => {},
  'universal-linux' => {'cap2' => '>= 0.2.2'},
  'universal-mingw-ucrt' => {'win32-security' => '>= 0.2.0'}
}

Run it before changing gem:create:

bundle exec rake clean gem:create gem:check

Expected: FAIL because gem:create still builds only one artifact.

  • Step 2: Implement deterministic specification loading and artifact creation

Define the shared list once in Rakefile:

GEMSPEC_FILES = %w[
  net-ping.gemspec
  net-ping-universal-linux.gemspec
  net-ping-universal-mingw-ucrt.gemspec
].freeze

Update gem:create to load all files before it builds any artifact:

specifications = GEMSPEC_FILES.map { |path| Gem::Specification.load(path) }
specifications.each { |spec| Gem::Package.build(spec) }

Retain the existing RubyGems pre-2.0 builder branch if support for it is still required. In either branch, load every specification before beginning the build loop.

Implement gem:check using Gem::Package.new(file).spec; use abort with the artifact filename and expected/actual values when a check fails.

Update gem:install to load the same specification list and select:

specific = specifications.find do |spec|
  spec.platform != Gem::Platform::RUBY && Gem::Platform.installable?(spec)
end
spec = specific || specifications.find { |item| item.platform == Gem::Platform::RUBY }

Install spec.file_name, not Dir['*.gem'].first; abort when no Ruby fallback specification is found.

  • Step 3: Run artifact validation to verify it passes

Run:

bundle exec rake gem:create gem:check

Expected: PASS and creation of net-ping-<version>.gem,
net-ping-<version>-universal-linux.gem, and
net-ping-<version>-universal-mingw-ucrt.gem.

  • Step 4: Verify local artifact selection

Run in a temporary gem repository so the developer's normal gem installation
is unchanged:

tmp_gem_home=$(mktemp -d)
GEM_HOME="$tmp_gem_home" GEM_PATH="$tmp_gem_home" bundle exec rake gem:install
GEM_HOME="$tmp_gem_home" GEM_PATH="$tmp_gem_home" gem specification net-ping platform
rm -rf "$tmp_gem_home"

Expected on Linux: universal-linux; on Windows: universal-mingw-ucrt; on
other platforms: ruby.

  • Step 5: Re-run tests

Run:

bundle exec rake test

Expected: PASS.

Task 3: Enforce packaging in CI and document the behavior

Files:

  • Modify: .github/workflows/test.yml:19-28
  • Modify: README.md:4-15
  • Modify: CHANGES:1-13

Interfaces:

  • Consumes: gem:create and gem:check from Task 2.

  • Produces: CI verification on both matrix operating systems and accurate end-user dependency documentation.

  • Step 1: Add the CI packaging verification step

After the existing bundle exec rake test step, add:

      - run: bundle exec rake gem:create gem:check

Do not add RubyGems credentials, publishing actions, or tag triggers.

  • Step 2: Document platform-specific dependency resolution

Replace the prerequisite list’s unconditional win32-security (MS Windows only) wording with text that states RubyGems installs win32-security from
the Windows artifact and cap2 from the Linux artifact when installing
net-ping. Keep the installation command gem install net-ping.

At the beginning of CHANGES, add:

== Next Release
* Publish platform-specific gem metadata so Linux installs cap2 and current
  Windows RubyInstaller installs win32-security for ICMP support.
  • Step 3: Run targeted packaging and full test verification

Run:

bundle exec rake gem:create gem:check
bundle exec rake test

Expected: both commands PASS.

  • Step 4: Inspect the final diff without committing

Run:

git diff --check
git diff -- net-ping.gemspec net-ping-universal-linux.gemspec \
  net-ping-universal-mingw-ucrt.gemspec Rakefile \
  test/test_net_ping_gem_packaging.rb test/test_net_ping.rb \
  .github/workflows/test.yml README.md CHANGES

Expected: only the planned packaging, CI, test, and documentation changes;
no whitespace errors and no git commit.

Plan Self-Review

  • Spec coverage: Task 1 creates the three required artifacts and assigns
    their dependencies; Task 2 validates metadata and selects a local artifact;
    Task 3 runs validation in CI and documents the behavior. CI publishing,
    legacy mingw32, and runtime protocol changes are excluded.
  • Placeholder scan: No unresolved item, unspecified test, or ambiguous
    implementation step remains.
  • Consistency: The gemspec filenames, platform names, dependency
    versions, and Rake task names are identical across all tasks.

Copilot AI review requested due to automatic review settings July 25, 2026 08:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR addresses platform-specific runtime dependencies by publishing separate gem artifacts per platform (Ruby, universal-linux, universal-mingw-ucrt) so RubyGems can select the correct dependency set at install time, and adds CI/test coverage around artifact validation.

Changes:

  • Introduces platform-specific gemspecs for universal-linux and universal-mingw-ucrt and removes platform-conditional deps from the main gemspec.
  • Adds Rake tasks and a new packaging-focused test suite to build and validate gem artifacts (platform + runtime deps).
  • Updates CI to build/check artifacts and updates docs/changelog to reflect platform-specific dependency selection.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
test/test_net_ping.rb Runs the new gem packaging test suite as part of the overall test entrypoint.
test/test_net_ping_gem_packaging.rb Adds tests that build artifacts, validate platform/runtime deps, and exercise new Rake tasks.
README.md Updates prerequisites text to reflect platform-selected dependencies via gem artifacts.
Rakefile Adds multi-artifact build, validation (gem:check), and install logic for platform-specific artifacts.
net-ping.gemspec Removes platform-conditional runtime deps and excludes generated .gem files from spec.files.
net-ping-universal-linux.gemspec Adds a Linux platform gemspec that includes cap2 at runtime.
net-ping-universal-mingw-ucrt.gemspec Adds a Windows (mingw-ucrt) platform gemspec that includes win32-security at runtime.
Gemfile Ensures Windows development/test installs include win32-security.
CHANGES Documents the upcoming release behavior change for platform-specific dependency selection.
.github/workflows/test.yml Extends CI to build and validate gem artifacts after tests.
Comments suppressed due to low confidence (1)

Rakefile:76

  • gem:check currently trusts the artifact filename from EXPECTED_GEM_ARTIFACTS and doesn’t verify the artifact exists. If the artifact name changes (e.g., version bump) or artifacts weren’t built, this will raise a less helpful exception. It’s more robust to pair loaded specs with expectations by gemspec path and use spec.file_name as the artifact to validate (and abort with a clear message if missing).
    EXPECTED_GEM_ARTIFACTS.each do |_, expectation|
      artifact, expected_platform, expected_dependencies = expectation
      packaged_spec = Gem::Package.new(artifact).spec

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread Rakefile Outdated
Comment thread net-ping-universal-mingw-ucrt.gemspec
@eitoball
eitoball force-pushed the issue-31-platform-gems branch 2 times, most recently from 3ab0e2d to ca31ae2 Compare August 1, 2026 02:47
@eitoball
eitoball requested a review from Copilot August 1, 2026 02:50

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Suppressed comments (1)

README.md:7

  • The Windows platform gem now installs both win32-security and win32ole (per the mingw-ucrt gemspec and packaging expectations), but the prerequisites list only mentions win32-security. Adding win32ole here keeps the documentation aligned with what RubyGems will actually install for Windows users (and avoids confusion when WMI ping functionality requires it).
  * win32-security (installed from the Windows gem artifact)
  * cap2 (installed from the Linux gem artifact)

Comment thread Gemfile
@eitoball
eitoball force-pushed the issue-31-platform-gems branch 2 times, most recently from 726c343 to bc8e0ca Compare August 1, 2026 05:52
@eitoball
eitoball requested a review from Copilot August 1, 2026 05:52

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Rakefile:85

  • gem:check calls Gem::Specification.load(path).file_name without checking whether the gemspec loaded successfully and whether the artifact exists. If a gemspec can’t be loaded or artifacts weren’t built, this will raise a NoMethodError/IO error instead of failing with a clear, actionable message.
    EXPECTED_GEM_METADATA.each do |path, expectation|
      expected_platform, expected_dependencies = expectation
      artifact = Gem::Specification.load(path).file_name
      packaged_spec = Gem::Package.new(artifact).spec

README.md:7

  • The prerequisites list documents the platform-selected dependencies, but it omits win32ole, which is now also installed from the Windows gem artifact (and is required by lib/net/ping/wmi.rb). This can mislead Windows users about what gets installed automatically.
  * win32-security (installed from the Windows gem artifact)
  * cap2 (installed from the Linux gem artifact)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (4)

net-ping-universal-linux.gemspec:1

  • The overlay gemspec loads the base gemspec using a relative path. If someone runs gem build from a different working directory, Gem::Specification.load('net-ping.gemspec') may fail to find the base spec. Use a path relative to this file (__dir__) so builds are deterministic regardless of CWD.
spec = Gem::Specification.load('net-ping.gemspec').dup

net-ping-universal-mingw-ucrt.gemspec:1

  • The overlay gemspec loads the base gemspec using a relative path. If someone runs gem build from a different working directory, Gem::Specification.load('net-ping.gemspec') may fail to find the base spec. Use a path relative to this file (__dir__) so builds are deterministic regardless of CWD.
spec = Gem::Specification.load('net-ping.gemspec').dup

Rakefile:86

  • gem:check assumes Gem::Specification.load(path) always succeeds; if a gemspec is missing or fails to load, this will raise a NoMethodError on file_name instead of failing with a clear message. Abort explicitly when a gemspec can't be loaded.
    EXPECTED_GEM_METADATA.each do |path, expectation|
      expected_platform, expected_dependencies = expectation
      artifact = Gem::Specification.load(path).file_name
      packaged_spec = Gem::Package.new(artifact).spec
      actual_dependencies = packaged_spec.runtime_dependencies.each_with_object({}) do |dependency, memo|

Rakefile:63

  • The task description says it "Create[s] the net-ping gem", but the task now builds three platform-specific artifacts. Updating the description helps avoid confusion when discovering tasks via rake -T.
  desc 'Create the net-ping gem'

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (2)

test/test_net_ping_gem_packaging.rb:50

  • omit inside the per-spec loop aborts the entire test on the first non-round-trippable platform string, which also skips the runtime-dependency assertions for the remaining artifacts. This can let dependency metadata regressions slip by on older RubyGems versions.

Instead of omitting the whole test, skip only the platform assertion when round-tripping is unsupported and still assert the dependency hash.

      if platform_round_trips_for_test?(expected_platform)
        assert_equal(expected_platform, packaged_spec.platform.to_s, path)
      else
        omit("this RubyGems (#{Gem::VERSION}) cannot round-trip the #{expected_platform.inspect} platform string")
      end

Rakefile:88

  • gem:check assumes Gem::Specification.load(path) always returns a spec. If a gemspec is missing/invalid (or someone runs rake gem:check without having all files present), this will raise NoMethodError on nil.file_name rather than failing with a clear message.

Fail fast with an explicit abort when the gemspec can't be loaded, and also consider a clear error when the built artifact file is missing.

    EXPECTED_GEM_METADATA.each do |path, expectation|
      expected_platform, expected_dependencies = expectation
      artifact = Gem::Specification.load(path).file_name
      packaged_spec = Gem::Package.new(artifact).spec
      actual_dependencies = packaged_spec.runtime_dependencies.each_with_object({}) do |dependency, memo|

Ruby 2.7's bundled RubyGems doesn't recognize the "mingw-ucrt" OS
token, so re-parsing the platform string during gem packaging
normalizes it to "universal-unknown" instead of
"universal-mingw-ucrt". Skip that specific platform check when the
running RubyGems can't round-trip the string, in both gem:check and
its test coverage.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated no new comments.

Suppressed comments (2)

Rakefile:85

  • gem:check calls Gem::Specification.load(path).file_name without checking whether the gemspec actually loaded. If Gem::Specification.load returns nil (missing file, syntax error, etc.), this will raise a NoMethodError and produce a hard-to-read stack trace instead of a clear failure.

Load the spec once, abort with a clear message when it can’t be loaded, and optionally fail fast when the expected artifact file is missing.

    EXPECTED_GEM_METADATA.each do |path, expectation|
      expected_platform, expected_dependencies = expectation
      artifact = Gem::Specification.load(path).file_name
      packaged_spec = Gem::Package.new(artifact).spec

README.md:8

  • The prerequisites list now mentions platform-specific gems, but it doesn’t explicitly state that RubyGems will automatically select the correct platform-specific artifact during gem install net-ping. As written, it can be read as “install these gems manually on every platform”, which contradicts the goal of making dependency selection automatic.

Consider rewording this section to describe the automatic selection, and list the platform-specific dependencies per platform.

## Prerequisites
  * ffi
  * win32-security (installed from the Windows gem artifact)
  * win32ole (installed from the Windows gem artifact)
  * cap2 (installed from the Linux gem artifact)

@eitoball
eitoball merged commit ee8378a into master Aug 1, 2026
15 checks passed
@eitoball
eitoball deleted the issue-31-platform-gems branch August 1, 2026 07:14
@eitoball eitoball mentioned this pull request Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Windows gem dependency isn't properly set

2 participants