Merge pull request #23400 from Homebrew/detect-app-case

cask/audit: detect incorrect app case
This commit is contained in:
Mike McQuaid
2026-08-03 08:11:14 +00:00
committed by GitHub
2 changed files with 136 additions and 0 deletions
+49
View File
@@ -686,6 +686,55 @@ module Cask
end
end
sig { void }
def audit_artifact_case
return if (url = cask.url).nil?
return unless online?
odebug "Auditing artifact case"
extract_artifacts(include_manual_installers: true) do |artifacts, tmpdir|
artifacts.each do |artifact|
source = case artifact
when Artifact::Pkg, Artifact::Installer
artifact.path
else
artifact.source
end
source = if source.to_s.start_with?("#{cask.appdir}/")
Pathname(source.to_s.delete_prefix("#{cask.appdir}/"))
elsif source.absolute?
source.relative_path_from(cask.staged_path)
else
source
end
components = source.each_filename.to_a
current = tmpdir
on_disk = []
components.each do |component|
break unless current.directory?
children = current.children.map { |child| child.basename.to_s }
match = children.find { |name| name == component } ||
children.find { |name| name.casecmp?(component) }
break if match.nil?
on_disk << match
current /= match
end
next if on_disk.length != components.length
next if on_disk == components
add_error "Artifact #{source} does not match the case of the extracted " \
"#{File.join(on_disk)}; this fails on case-sensitive filesystems.",
location: url.location
end
end
end
sig { void }
def audit_rosetta
return if (url = cask.url).nil?
+87
View File
@@ -1205,6 +1205,93 @@ RSpec.describe Cask::Audit, :cask do
end
end
describe "artifact case checks" do
let(:online) { true }
let(:only) { ["artifact_case"] }
let(:tmpdir) { mktmpdir }
let(:cask) do
Cask::Cask.new("artifact-case") do
version "1.0"
sha256 :no_check
url "https://brew.sh/artifact-case.zip"
name "Artifact Case"
homepage "https://brew.sh/"
app "artifact case.app"
end
end
before do
allow(audit).to receive(:extract_artifacts).and_yield(cask.artifacts, tmpdir)
end
context "when the case matches" do
before { (tmpdir/"artifact case.app").mkpath }
it { is_expected.to pass }
end
context "when the case does not match" do
before { (tmpdir/"Artifact Case.app").mkpath }
it { is_expected.to error_with(/does not match the case of the extracted/) }
end
context "when both cases are present on disk" do
# Both spellings cannot be created on a case-insensitive filesystem.
before do
allow(tmpdir).to receive(:children)
.and_return([tmpdir/"Artifact Case.app", tmpdir/"artifact case.app"])
end
it { is_expected.to pass }
end
context "when the artifact is missing" do
it { is_expected.to pass }
end
context "when a binary in the appdir has the wrong case" do
let(:cask) do
Cask::Cask.new("artifact-case") do
version "1.0"
sha256 :no_check
url "https://brew.sh/artifact-case.zip"
name "Artifact Case"
homepage "https://brew.sh/"
app "Artifact Case.app"
binary "#{appdir}/Artifact Case.app/Contents/MacOS/artifact"
end
end
before do
(tmpdir/"Artifact Case.app/Contents/MacOS").mkpath
FileUtils.touch tmpdir/"Artifact Case.app/Contents/MacOS/Artifact"
end
it { is_expected.to error_with(/does not match the case of the extracted/) }
end
context "when a manual installer has the wrong case" do
let(:cask) do
Cask::Cask.new("artifact-case") do
version "1.0"
sha256 :no_check
url "https://brew.sh/artifact-case.zip"
name "Artifact Case"
homepage "https://brew.sh/"
installer manual: "Artifact Case.app"
end
end
before { (tmpdir/"Artifact Case.APP").mkpath }
it { is_expected.to error_with(/does not match the case of the extracted/) }
end
end
describe "minimum OS checks" do
let(:online) { true }
let(:only) { ["min_os"] }