Skip to content

Commit

Permalink
fix: execute commands consistently for paths with spaces
Browse files Browse the repository at this point in the history
Note that this is a pretty heavy refactoring, where code was moved
from `exe/tailwindcss` to `lib/tailwindcss/commands.rb` where we can
more easily run unit tests on it.

Note also that we no longer use Shellwords to build command strings, a
library which does not generate correct strings on Windows
platforms. Instead we consistently use arrays of command arguments,
which can be passed to `exec` or `system` however we see fit.

The wrapper script conditionally uses `system` on windows platforms
because `exec` can't find the executable (see related issue at
rubys/sprockets-esbuild#4).

Finally, note that the rake tasks no longer use the `exe/tailwindcss`
wrapper script, and instead use the binary executable directly. We can
reverse this decision if we ever decide to support manually-installed
tailwindcss somewhere on the $PATH; but because previously the rake
tasks hardcoded the path/to/exe/tailwindcss, we're not introducing any
new constraints by skipping the wrapper.
  • Loading branch information
flavorjones committed Sep 3, 2022
1 parent 30495ab commit 76e5769
Show file tree
Hide file tree
Showing 5 changed files with 162 additions and 48 deletions.
55 changes: 14 additions & 41 deletions exe/tailwindcss
Original file line number Diff line number Diff line change
@@ -1,46 +1,19 @@
#! /usr/bin/env ruby
# because rubygems shims assume a gem's executables are Ruby

require "shellwords"
require "tailwindcss/upstream"

supported_platforms = Tailwindcss::Upstream::NATIVE_PLATFORMS.keys
platform = [:cpu, :os].map { |m| Gem::Platform.local.send(m) }.join("-")

if supported_platforms.none? { |supported_platform| Gem::Platform.match(supported_platform) }
STDERR.puts(<<~ERRMSG)
ERROR: tailwindcss-rails does not support the #{platform} platform
Please install tailwindcss following instructions at https://tailwindcss.com/docs/installation
ERRMSG
require "tailwindcss/commands"

begin
command = [Tailwindcss::Commands.executable, *ARGV]
puts command.inspect
if Gem.win_platform?
# use system rather than exec as exec inexplicably fails to find the executable on Windows
# see related https://github.com/rubys/sprockets-esbuild/pull/4
system(*command, exception: true)
else
exec(*command)
end
rescue Tailwindcss::Commands::UnsupportedPlatformException, Tailwindcss::Commands::ExecutableNotFoundException => e
STDERR.puts("ERROR: " + e.message)
exit 1
end

exe_path = Dir.glob(File.join(__dir__, "*", "tailwindcss")).find do |f|
Gem::Platform.match(File.basename(File.dirname(f)))
end
if exe_path.nil?
STDERR.puts(<<~ERRMSG)
ERROR: Cannot find the tailwindcss executable for #{platform} in #{__dir__}
If you're using bundler, please make sure you're on the latest bundler version:
gem install bundler
bundle update --bundler
Then make sure your lock file includes this platform by running:
bundle lock --add-platform #{platform}
bundle install
See `bundle lock --help` output for details.
If you're still seeing this message after taking those steps, try running
`bundle config` and ensure `force_ruby_platform` isn't set to `true`. See
https://github.com/rails/tailwindcss-rails#check-bundle_force_ruby_platform
for more details.
ERRMSG
exit 1
end

command = Shellwords.join([exe_path, ARGV].flatten)
puts "+ #{command}"
exec(command)
7 changes: 4 additions & 3 deletions lib/tailwindcss-rails.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
module Tailwindcss
end

require "tailwindcss/upstream"
require "tailwindcss/version"
require "tailwindcss/engine"
require_relative "tailwindcss/upstream"
require_relative "tailwindcss/version"
require_relative "tailwindcss/engine"
require_relative "tailwindcss/commands"
73 changes: 73 additions & 0 deletions lib/tailwindcss/commands.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
require_relative "upstream"

module Tailwindcss
module Commands
# raised when the host platform is not supported by upstream tailwindcss's binary releases
class UnsupportedPlatformException < StandardError
end

# raised when the tailwindcss executable could not be found where we expected it to be
class ExecutableNotFoundException < StandardError
end

class << self
def platform
[:cpu, :os].map { |m| Gem::Platform.local.send(m) }.join("-")
end

def executable(
exe_path: File.expand_path(File.join(__dir__, "..", "..", "exe"))
)
if Tailwindcss::Upstream::NATIVE_PLATFORMS.keys.none? { |p| Gem::Platform.match(p) }
raise UnsupportedPlatformException, <<~MESSAGE
tailwindcss-rails does not support the #{platform} platform
Please install tailwindcss following instructions at https://tailwindcss.com/docs/installation
MESSAGE
end

exe_path = Dir.glob(File.expand_path(File.join(exe_path, "*", "tailwindcss"))).find do |f|
Gem::Platform.match(File.basename(File.dirname(f)))
end

if exe_path.nil?
raise ExecutableNotFoundException, <<~MESSAGE
Cannot find the tailwindcss executable for #{platform} in #{exe_path}
If you're using bundler, please make sure you're on the latest bundler version:
gem install bundler
bundle update --bundler
Then make sure your lock file includes this platform by running:
bundle lock --add-platform #{platform}
bundle install
See `bundle lock --help` output for details.
If you're still seeing this message after taking those steps, try running
`bundle config` and ensure `force_ruby_platform` isn't set to `true`. See
https://github.com/rails/tailwindcss-rails#check-bundle_force_ruby_platform
for more details.
MESSAGE
end

exe_path
end

def compile_command(**kwargs)
[
executable(**kwargs),
"-i", Rails.root.join("app/assets/stylesheets/application.tailwind.css").to_s,
"-o", Rails.root.join("app/assets/builds/tailwind.css").to_s,
"-c", Rails.root.join("config/tailwind.config.js").to_s,
"--minify",
]
end

def watch_command(**kwargs)
compile_command(**kwargs) << "-w"
end
end
end
end
10 changes: 6 additions & 4 deletions lib/tasks/build.rake
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
TAILWIND_COMPILE_COMMAND = "#{RbConfig.ruby} #{Pathname.new(__dir__).to_s}/../../exe/tailwindcss -i '#{Rails.root.join("app/assets/stylesheets/application.tailwind.css")}' -o '#{Rails.root.join("app/assets/builds/tailwind.css")}' -c '#{Rails.root.join("config/tailwind.config.js")}' --minify"

namespace :tailwindcss do
desc "Build your Tailwind CSS"
task :build do
system(TAILWIND_COMPILE_COMMAND, exception: true)
command = Tailwindcss::Commands.compile_command
puts command.inspect
system(*command, exception: true)
end

desc "Watch and build your Tailwind CSS on file changes"
task :watch do
system "#{TAILWIND_COMPILE_COMMAND} -w"
command = Tailwindcss::Commands.watch_command
puts command.inspect
system(*command)
end
end

Expand Down
65 changes: 65 additions & 0 deletions test/lib/tailwindcss/commands_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
require "test_helper"
require "minitest/mock"

class Tailwindcss::CommandsTest < ActiveSupport::TestCase
test ".platform is a string containing just the cpu and os (not the version)" do
expected = "#{Gem::Platform.local.cpu}-#{Gem::Platform.local.os}"
assert_equal(expected, Tailwindcss::Commands.platform)
end

def mock_exe_directory(platform)
Dir.mktmpdir do |dir|
FileUtils.mkdir(File.join(dir, platform))
path = File.join(dir, platform, "tailwindcss")
FileUtils.touch(path)
Gem::Platform.stub(:match, true) do
yield(dir, path)
end
end
end

test ".executable returns the absolute path to the binary" do
mock_exe_directory("sparc-solaris2.8") do |dir, executable|
expected = File.expand_path(File.join(dir, "sparc-solaris2.8", "tailwindcss"))
assert_equal(expected, executable, "assert on setup")
assert_equal(expected, Tailwindcss::Commands.executable(exe_path: dir))
end
end

test ".executable raises UnsupportedPlatformException when we're not on a supported platform" do
Gem::Platform.stub(:match, false) do # nothing is supported
assert_raises(Tailwindcss::Commands::UnsupportedPlatformException) do
Tailwindcss::Commands.executable
end
end
end

test ".executable raises ExecutableNotFoundException when we can't find the executable we expect" do
Dir.mktmpdir do |dir| # empty directory
assert_raises(Tailwindcss::Commands::ExecutableNotFoundException) do
Tailwindcss::Commands.executable(exe_path: dir)
end
end
end

test ".compile_command" do
mock_exe_directory("sparc-solaris2.8") do |dir, executable|
Rails.stub(:root, File) do # Rails.root won't work in this test suite
actual = Tailwindcss::Commands.compile_command(exe_path: dir)
assert_kind_of(Array, actual)
assert_equal(executable, actual.first)
end
end
end

test ".watch_command" do
mock_exe_directory("sparc-solaris2.8") do |dir, executable|
Rails.stub(:root, File) do # Rails.root won't work in this test suite
actual = Tailwindcss::Commands.watch_command(exe_path: dir)
assert_kind_of(Array, actual)
assert_equal(executable, actual.first)
assert_includes(actual, "-w")
end
end
end
end

0 comments on commit 76e5769

Please sign in to comment.