Skip to content
6 changes: 4 additions & 2 deletions lib/install/bin/diff-bundler-config
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ def shakapacker_executable_candidates(executable)
end

def shakapacker_find_executable(executable)
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR, -1).each do |path|
search_path = path.empty? ? Dir.pwd : path

shakapacker_executable_candidates(executable).each do |candidate|
executable_path = File.join(path, candidate)
executable_path = File.join(search_path, candidate)
return executable_path if File.file?(executable_path) && File.executable?(executable_path)
end
end
Expand Down
6 changes: 4 additions & 2 deletions lib/install/bin/shakapacker-config
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ def shakapacker_executable_candidates(executable)
end

def shakapacker_find_executable(executable)
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR, -1).each do |path|
search_path = path.empty? ? Dir.pwd : path
Comment thread
justin808 marked this conversation as resolved.

shakapacker_executable_candidates(executable).each do |candidate|
executable_path = File.join(path, candidate)
executable_path = File.join(search_path, candidate)
return executable_path if File.file?(executable_path) && File.executable?(executable_path)
end
end
Expand Down
18 changes: 13 additions & 5 deletions package/configExporter/cli.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
// This will be a substantial file - the main CLI entry point
// Originally migrated from bin/export-bundler-config, now bin/shakapacker-config

import { existsSync, readFileSync, writeFileSync } from "fs"
import {
chmodSync,
existsSync,
mkdirSync,
readFileSync,
writeFileSync
} from "fs"
import { resolve, dirname, sep, delimiter, basename } from "path"
import { inspect } from "util"
import { load as loadYaml } from "js-yaml"
Expand Down Expand Up @@ -510,10 +516,10 @@ function runInitCommand(options: ExportOptions): number {
return 0
}

function createBinStub(binStubPath: string): void {
// Exported for test use only: verifies generated content matches lib/install/bin/* binstubs.
Comment thread
justin808 marked this conversation as resolved.
Outdated
export function createBinStub(binStubPath: string): void {
const binDir = dirname(binStubPath)
const packageScript = `${basename(binStubPath)}.cjs`
const { mkdirSync, chmodSync } = require("fs")

// Ensure bin directory exists
if (!existsSync(binDir)) {
Expand Down Expand Up @@ -550,9 +556,11 @@ def shakapacker_executable_candidates(executable)
end

def shakapacker_find_executable(executable)
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR, -1).each do |path|
search_path = path.empty? ? Dir.pwd : path

shakapacker_executable_candidates(executable).each do |candidate|
executable_path = File.join(path, candidate)
executable_path = File.join(search_path, candidate)
return executable_path if File.file?(executable_path) && File.executable?(executable_path)
end
end
Expand Down
6 changes: 4 additions & 2 deletions spec/dummy/bin/shakapacker-config
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,11 @@ def shakapacker_executable_candidates(executable)
end

def shakapacker_find_executable(executable)
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR).each do |path|
ENV.fetch("PATH", "").split(File::PATH_SEPARATOR, -1).each do |path|
search_path = path.empty? ? Dir.pwd : path

shakapacker_executable_candidates(executable).each do |candidate|
executable_path = File.join(path, candidate)
executable_path = File.join(search_path, candidate)
return executable_path if File.file?(executable_path) && File.executable?(executable_path)
end
end
Expand Down
4 changes: 3 additions & 1 deletion spec/shakapacker/binstub_sync_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@

expect(dummy_content).to eq(install_content),
"spec/dummy/bin/shakapacker-config and lib/install/bin/shakapacker-config have diverged. " \
Comment thread
justin808 marked this conversation as resolved.
"Update both files to keep them in sync."
"Update all four copies (lib/install/bin/shakapacker-config, lib/install/bin/diff-bundler-config, " \
"spec/dummy/bin/shakapacker-config, and the createBinStub template in package/configExporter/cli.ts) " \
"to keep them in sync."
end

# lib/install/bin/diff-bundler-config and lib/install/bin/shakapacker-config share
Expand Down
77 changes: 66 additions & 11 deletions spec/shakapacker/helper_binstubs_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ def install_fake_node_script(app_path, command)
FileUtils.chmod(0o755, script_path)
end

def real_node_path
ENV.fetch("PATH").split(File::PATH_SEPARATOR)
Comment thread
justin808 marked this conversation as resolved.
Outdated
Comment thread
justin808 marked this conversation as resolved.
Outdated
.map { |dir| File.join(dir, "node") }
.find { |candidate| File.file?(candidate) && File.executable?(candidate) }
end

%w[shakapacker-config diff-bundler-config].each do |command|
it "runs #{command} through a CommonJS package script when the app is ESM" do
Dir.mktmpdir("shakapacker-binstub-") do |app_path|
Expand Down Expand Up @@ -160,11 +166,9 @@ def install_fake_node_script(app_path, command)
FileUtils.mkdir_p(File.join(app_path, "bin"))
install_fake_node_script(app_path, command)

real_node_path = ENV.fetch("PATH").split(File::PATH_SEPARATOR).map { |path| File.join(path, "node") }.find do |path|
File.file?(path) && File.executable?(path)
end
node_path = real_node_path

skip "node not found in PATH" unless real_node_path
skip "node not found in PATH" unless node_path

fake_bin_path = File.join(app_path, "fake-bin")
FileUtils.mkdir_p(fake_bin_path)
Expand All @@ -176,7 +180,7 @@ def install_fake_node_script(app_path, command)
echo probed >> "$SHAKAPACKER_NODE_PROBE_OUTPUT"
exit 0
fi
exec #{real_node_path.shellescape} "$@"
exec #{node_path.shellescape} "$@"
SH
FileUtils.chmod(0o755, fake_node_path)

Expand All @@ -200,6 +204,55 @@ def install_fake_node_script(app_path, command)
end
end

{
"leading" => "#{File::PATH_SEPARATOR}/nonexistent",
"trailing" => "/nonexistent#{File::PATH_SEPARATOR}"
}.each do |position, path_value|
it "honors a #{position} empty PATH entry as the current directory for #{command}" do
Dir.mktmpdir("shakapacker-binstub-") do |app_path|
File.write(File.join(app_path, "Gemfile"), "")
FileUtils.mkdir_p(File.join(app_path, "bin"))
install_fake_node_script(app_path, command)

node_path = real_node_path
skip "node not found in PATH" unless node_path

launch_path = File.join(app_path, "launch")
FileUtils.mkdir_p(launch_path)
fake_node_path = File.join(launch_path, "node")
File.write(fake_node_path, <<~SH)
#!/bin/sh
exec #{node_path.shellescape} "$@"
SH
FileUtils.chmod(0o755, fake_node_path)

binstub_path = File.join(app_path, "bin", command)
FileUtils.cp(File.join(gem_root, "lib", "install", "bin", command), binstub_path)
FileUtils.chmod(0o755, binstub_path)

output_path = File.join(app_path, "binstub-output.json")
_stdout, stderr, status = Bundler.with_unbundled_env do
Open3.capture3(
{
"BUNDLE_GEMFILE" => nil,
"PATH" => path_value,
"RUBYOPT" => nil,
"SHAKAPACKER_BINSTUB_OUTPUT" => output_path
},
RbConfig.ruby,
binstub_path,
chdir: launch_path
)
end

expect(status).to be_success, stderr
expect(JSON.parse(File.read(output_path))).to include(
"cwd" => File.realpath(app_path)
)
end
Comment thread
justin808 marked this conversation as resolved.
end
end

it "exits with an error when #{command}'s package script is missing" do
Dir.mktmpdir("shakapacker-binstub-") do |app_path|
File.write(File.join(app_path, "Gemfile"), "")
Expand Down Expand Up @@ -232,12 +285,14 @@ def install_fake_node_script(app_path, command)
FileUtils.cp(File.join(gem_root, "lib", "install", "bin", command), binstub_path)
FileUtils.chmod(0o755, binstub_path)

_stdout, stderr, status = Open3.capture3(
{ "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil, "PATH" => "/nonexistent" },
RbConfig.ruby,
binstub_path,
chdir: app_path
)
_stdout, stderr, status = Bundler.with_unbundled_env do
Open3.capture3(
{ "BUNDLE_GEMFILE" => nil, "RUBYOPT" => nil, "PATH" => "/nonexistent" },
RbConfig.ruby,
binstub_path,
chdir: app_path
)
end

expect(status.exitstatus).to eq(1)
expect(stderr).to include('[Shakapacker] Could not find Node.js executable "node"')
Expand Down
51 changes: 51 additions & 0 deletions test/configExporter/createBinStub.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const {
accessSync,
constants,
mkdtempSync,
readFileSync,
rmSync
} = require("fs")
const { join, resolve } = require("path")
const { tmpdir } = require("os")
const { createBinStub } = require("../../package/configExporter/cli")

const gemRoot = resolve(__dirname, "../..")

// The Ruby logic in lib/install/bin/shakapacker-config and
// lib/install/bin/diff-bundler-config is also duplicated inside the
// `createBinStub` template in package/configExporter/cli.ts. The Ruby spec
// (spec/shakapacker/binstub_sync_spec.rb) keeps the three checked-in copies
// (install template, install diff template, and the dummy app's binstub)
// honest, but it cannot reach into the JS template. This test closes that
// gap by invoking createBinStub for both helper names and asserting the
// generated content matches the corresponding lib/install/bin/* file.
describe("createBinStub template parity", () => {
let tmp

beforeEach(() => {
tmp = mkdtempSync(join(tmpdir(), "shakapacker-createBinStub-"))
})

afterEach(() => {
Comment thread
justin808 marked this conversation as resolved.
rmSync(tmp, { recursive: true, force: true })
})

// diff-bundler-config is a white-box parity case; production init only
// generates shakapacker-config.
test.each(["shakapacker-config", "diff-bundler-config"])(
"generates lib/install/bin/%s byte-for-byte",
(binstubName) => {
const generatedPath = join(tmp, "bin", binstubName)
createBinStub(generatedPath)
Comment thread
justin808 marked this conversation as resolved.

const generated = readFileSync(generatedPath, "utf8")
const installed = readFileSync(
join(gemRoot, "lib", "install", "bin", binstubName),
"utf8"
)

expect(generated).toBe(installed)
Comment thread
justin808 marked this conversation as resolved.
expect(() => accessSync(generatedPath, constants.X_OK)).not.toThrow()
}
)
})
Loading