Skip to content

Commit 3234124

Browse files
authored
Merge pull request #22377 from Homebrew/quarantine-ffi
Convert quarantine script to use FFI rather than Swift
2 parents 46c9bd3 + 4dc0a0a commit 3234124

26 files changed

Lines changed: 1121 additions & 99 deletions

Library/Homebrew/cask/quarantine.rb

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ def self.xattr
4848
end
4949
private_class_method :xattr
5050

51+
sig { returns(T::Boolean) }
52+
def self.xattr_available?
53+
xattr = self.xattr
54+
return false if xattr.nil?
55+
56+
system_command(xattr, args: ["-h"], print_stderr: false).success?
57+
end
58+
5159
sig { returns(T::Array[String]) }
5260
def self.swift_target_args
5361
["-target", "#{Hardware::CPU.arch}-apple-macosx#{MacOS.version}"]
@@ -59,17 +67,15 @@ def self.check_quarantine_support
5967
odebug "Checking quarantine support"
6068

6169
check_output = nil
62-
status = if xattr.nil? || !system_command(T.must(xattr), args: ["-h"], print_stderr: false).success?
70+
swift = self.swift
71+
status = if !xattr_available?
6372
odebug "There's no working version of `xattr` on this system."
6473
:xattr_broken
6574
elsif swift.nil?
6675
odebug "Swift is not available on this system."
6776
:no_swift
6877
else
69-
s = swift
70-
raise "unexpected nil swift" unless s
71-
72-
api_check = system_command(s,
78+
api_check = system_command(swift,
7379
args: [*swift_target_args, QUARANTINE_SCRIPT],
7480
print_stderr: false)
7581

@@ -205,9 +211,10 @@ def self.cask!(cask: nil, download_path: nil, action: true)
205211

206212
odebug "Quarantining #{download_path}"
207213

208-
raise "unexpected nil swift" unless swift
214+
swift = self.swift
215+
raise "unexpected nil swift" if swift.nil?
209216

210-
quarantiner = system_command(T.must(swift),
217+
quarantiner = system_command(swift,
211218
args: [
212219
*swift_target_args,
213220
QUARANTINE_SCRIPT,
@@ -272,10 +279,11 @@ def self.propagate(from: nil, to: nil)
272279
def self.copy_xattrs(from, to, command:)
273280
odebug "Copying xattrs from #{from} to #{to}"
274281

275-
raise "unexpected nil swift" unless swift
282+
swift = self.swift
283+
raise "unexpected nil swift" if swift.nil?
276284

277285
command.run!(
278-
T.must(swift),
286+
swift,
279287
args: [
280288
*swift_target_args,
281289
COPY_XATTRS_SCRIPT,

Library/Homebrew/cask/utils/trash.rb

Lines changed: 2 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -16,37 +16,7 @@ module Trash
1616
.returns([T::Array[String], T::Array[String]])
1717
}
1818
def self.trash(*paths, command: nil)
19-
swift_trash(*paths, command:)
20-
end
21-
22-
sig {
23-
params(paths: Pathname, command: T.nilable(T.class_of(SystemCommand)))
24-
.returns([T::Array[String], T::Array[String]])
25-
}
26-
def self.swift_trash(*paths, command: nil)
27-
return [[], []] if paths.empty?
28-
29-
stdout = system_command(HOMEBREW_LIBRARY_PATH/"cask/utils/trash.swift",
30-
args: paths,
31-
print_stderr: Homebrew::EnvConfig.developer?).stdout
32-
33-
trashed, _, untrashable = stdout.partition("\n")
34-
trashed = trashed.split(":")
35-
untrashable = untrashable.split(":")
36-
37-
trashed_with_permissions, untrashable = untrashable.partition do |path|
38-
Utils.gain_permissions(Pathname(path), ["-R"], SystemCommand) do
39-
system_command! HOMEBREW_LIBRARY_PATH/"cask/utils/trash.swift",
40-
args: [path],
41-
print_stderr: Homebrew::EnvConfig.developer?
42-
end
43-
44-
true
45-
rescue
46-
false
47-
end
48-
49-
[trashed + trashed_with_permissions, untrashable]
19+
freedesktop_trash(*paths)
5020
end
5121

5222
sig { params(paths: Pathname).returns([T::Array[String], T::Array[String]]) }
@@ -114,7 +84,7 @@ def self.trash_path(path, files_path:, info_path:)
11484
return
11585
end
11686
end
117-
private_class_method :swift_trash, :freedesktop_trash, :trash_path
87+
private_class_method :trash_path
11888
end
11989
end
12090
end
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# typed: strict
22
# frozen_string_literal: true
33

4+
require "extend/os/mac/cask/quarantine" if OS.mac?
45
require "extend/os/linux/cask/quarantine" if OS.linux?
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# typed: strict
22
# frozen_string_literal: true
33

4+
require "extend/os/mac/cask/utils/trash" if OS.mac?
45
require "extend/os/linux/cask/utils/trash" if OS.linux?
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "os/mac/ffi"
5+
6+
module OS
7+
module Mac
8+
module Cask
9+
module Quarantine
10+
COPY_XATTRS_RUBY = "require \"os/mac/ffi\"; MacOS::FFI.copy_xattrs(ARGV.fetch(0), ARGV.fetch(1))"
11+
12+
module ClassMethods
13+
extend T::Helpers
14+
include Kernel
15+
include ::Utils::Output::Mixin
16+
17+
requires_ancestor { ::Cask::Quarantine }
18+
19+
sig { returns([Symbol, T.nilable(String)]) }
20+
def check_quarantine_support
21+
return super unless ffi_quarantine?
22+
23+
odebug "Checking quarantine support"
24+
25+
status = if ::Cask::Quarantine.xattr_available?
26+
odebug "Quarantine is available via FFI."
27+
:quarantine_available
28+
else
29+
odebug "There's no working version of `xattr` on this system."
30+
:xattr_broken
31+
end
32+
[status, nil]
33+
end
34+
35+
sig { params(cask: T.nilable(::Cask::Cask), download_path: T.nilable(::Pathname), action: T::Boolean).void }
36+
def cask!(cask: nil, download_path: nil, action: true)
37+
return super unless ffi_quarantine?
38+
return if cask.nil? || download_path.nil?
39+
40+
return if ::Cask::Quarantine.detect(download_path)
41+
42+
odebug "Quarantining #{download_path}"
43+
44+
path_cf_string = MacOS::FFI::CoreFoundation.string_create(download_path.to_s)
45+
if path_cf_string.null?
46+
Kernel.raise ::Cask::CaskQuarantineError.new(download_path,
47+
"Failed to create CFString for path")
48+
end
49+
50+
path_cf_url = MacOS::FFI::CoreFoundation.url_create_with_file_system_path(path_cf_string)
51+
if path_cf_url.null?
52+
Kernel.raise ::Cask::CaskQuarantineError.new(download_path,
53+
"Failed to create CFURL for path")
54+
end
55+
56+
quarantine_agent_name = MacOS::FFI::CoreFoundation.string_create("Homebrew Cask")
57+
quarantine_data_url = MacOS::FFI::CoreFoundation.string_create(cask.url.to_s)
58+
quarantine_origin_url = MacOS::FFI::CoreFoundation.string_create(cask.homepage.to_s)
59+
if quarantine_agent_name.null? || quarantine_data_url.null? || quarantine_origin_url.null?
60+
Kernel.raise ::Cask::CaskQuarantineError.new(download_path,
61+
"Failed to create CFString for quarantine properties")
62+
end
63+
64+
quarantine_dictionary = MacOS::FFI::CoreFoundation.dictionary_create(
65+
MacOS::FFI::LaunchServices.quarantine_agent_name_key => quarantine_agent_name,
66+
MacOS::FFI::LaunchServices.quarantine_type_key => MacOS::FFI::LaunchServices.quarantine_type_web_download,
67+
MacOS::FFI::LaunchServices.quarantine_data_url_key => quarantine_data_url,
68+
MacOS::FFI::LaunchServices.quarantine_origin_url_key => quarantine_origin_url,
69+
)
70+
if quarantine_dictionary.null?
71+
Kernel.raise ::Cask::CaskQuarantineError.new(download_path, "Failed to create quarantine dictionary")
72+
end
73+
74+
success = MacOS::FFI::CoreFoundation.url_set_resource_property_for_key(
75+
path_cf_url,
76+
MacOS::FFI::CoreFoundation.url_quarantine_properties_key,
77+
quarantine_dictionary,
78+
)
79+
80+
return if success
81+
82+
Kernel.raise ::Cask::CaskQuarantineError.new(download_path, "Failed to set quarantine properties for URL")
83+
end
84+
85+
sig { params(from: ::Pathname, to: ::Pathname, command: T.class_of(::SystemCommand)).void }
86+
def copy_xattrs(from, to, command:)
87+
odebug "Copying xattrs from #{from} to #{to}"
88+
return super unless ffi_quarantine?
89+
90+
if to.writable?
91+
MacOS::FFI.copy_xattrs(from.to_s, to.to_s)
92+
return
93+
end
94+
95+
command.run!(
96+
HOMEBREW_BREW_FILE,
97+
args: [
98+
"ruby",
99+
"--",
100+
"-e",
101+
COPY_XATTRS_RUBY,
102+
from,
103+
to,
104+
],
105+
sudo: true,
106+
)
107+
end
108+
109+
private
110+
111+
sig { returns(T::Boolean) }
112+
def ffi_quarantine?
113+
# TODO: Expand FFI quarantine and xattr copying to all users when fully working.
114+
Homebrew::EnvConfig.developer?
115+
end
116+
end
117+
end
118+
end
119+
end
120+
end
121+
122+
Cask::Quarantine.singleton_class.prepend(OS::Mac::Cask::Quarantine::ClassMethods)
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "os/mac/ffi"
5+
6+
module OS
7+
module Mac
8+
module Cask
9+
module Utils
10+
module Trash
11+
TRASH_SCRIPT = T.let((HOMEBREW_LIBRARY_PATH/"cask/utils/trash.swift").freeze, ::Pathname)
12+
13+
module ClassMethods
14+
include ::SystemCommand::Mixin
15+
16+
sig {
17+
params(paths: ::Pathname, command: T.nilable(T.class_of(::SystemCommand)))
18+
.returns([T::Array[String], T::Array[String]])
19+
}
20+
def trash(*paths, command: nil)
21+
return swift_trash(*paths, command:) unless ffi_trash?
22+
23+
trashed, untrashable = MacOS::FFI::Foundation.trash_paths(paths.map(&:to_s))
24+
25+
trashed_with_permissions = T.let([], T::Array[String])
26+
still_untrashable = T.let([], T::Array[String])
27+
untrashable.each do |path|
28+
destination = T.let(nil, T.nilable(String))
29+
::Cask::Utils.gain_permissions(::Pathname.new(path), ["-R"], ::SystemCommand) do
30+
destination = MacOS::FFI::Foundation.trash_item(path)
31+
Kernel.raise if destination.nil?
32+
end
33+
34+
if destination.nil?
35+
still_untrashable << path
36+
else
37+
trashed_with_permissions << destination
38+
end
39+
rescue
40+
still_untrashable << path
41+
end
42+
43+
[trashed + trashed_with_permissions, still_untrashable]
44+
end
45+
46+
private
47+
48+
sig { returns(T::Boolean) }
49+
def ffi_trash?
50+
# TODO: Expand FFI trashing to all users when fully working.
51+
Homebrew::EnvConfig.developer?
52+
end
53+
54+
sig {
55+
params(paths: ::Pathname, command: T.nilable(T.class_of(::SystemCommand)))
56+
.returns([T::Array[String], T::Array[String]])
57+
}
58+
def swift_trash(*paths, command: nil)
59+
return [[], []] if paths.empty?
60+
61+
stdout = system_command(TRASH_SCRIPT,
62+
args: paths,
63+
print_stderr: Homebrew::EnvConfig.developer?).stdout
64+
65+
trashed, _, untrashable = stdout.partition("\n")
66+
trashed = trashed.split(":")
67+
untrashable = untrashable.split(":")
68+
69+
trashed_with_permissions = T.let([], T::Array[String])
70+
still_untrashable = T.let([], T::Array[String])
71+
untrashable.each do |path|
72+
retried_stdout = T.let(nil, T.nilable(String))
73+
::Cask::Utils.gain_permissions(::Pathname.new(path), ["-R"], ::SystemCommand) do
74+
retried_stdout = system_command!(TRASH_SCRIPT,
75+
args: [path],
76+
print_stderr: Homebrew::EnvConfig.developer?).stdout
77+
end
78+
79+
retried_trashed, = retried_stdout.to_s.partition("\n")
80+
trashed_with_permissions.concat(retried_trashed.split(":"))
81+
rescue
82+
still_untrashable << path
83+
end
84+
85+
[trashed + trashed_with_permissions, still_untrashable]
86+
end
87+
end
88+
end
89+
end
90+
end
91+
end
92+
end
93+
94+
Cask::Utils::Trash.singleton_class.prepend(OS::Mac::Cask::Utils::Trash::ClassMethods)

Library/Homebrew/extend/os/mac/linkage_checker.rb

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,28 +4,18 @@
44
module OS
55
module Mac
66
module LinkageChecker
7-
private
7+
extend T::Helpers
88

9-
sig { returns(T::Boolean) }
10-
def system_libraries_exist_in_cache?
11-
# In macOS Big Sur and later, system libraries do not exist on-disk and instead exist in a cache.
12-
MacOS.version >= :big_sur
13-
end
9+
requires_ancestor { ::LinkageChecker }
10+
11+
private
1412

1513
sig { params(dylib: String).returns(T::Boolean) }
1614
def dylib_found_in_shared_cache?(dylib)
17-
Kernel.require "fiddle"
18-
@dyld_shared_cache_contains_path ||= T.let(begin
19-
libc = Fiddle.dlopen("/usr/lib/libSystem.B.dylib")
20-
21-
Fiddle::Function.new(
22-
libc["_dyld_shared_cache_contains_path"],
23-
[Fiddle::TYPE_CONST_STRING],
24-
Fiddle::TYPE_BOOL,
25-
)
26-
end, T.nilable(Fiddle::Function))
15+
return false if MacOS.version < :big_sur
2716

28-
@dyld_shared_cache_contains_path.call(dylib)
17+
require "os/mac/ffi"
18+
MacOS::FFI.dyld_shared_cache_contains_path(dylib)
2919
end
3020
end
3121
end

0 commit comments

Comments
 (0)