mirror of
https://github.com/Homebrew/brew.git
synced 2025-07-14 16:09:03 +08:00
Use a Formulary
-like approach to load Casks.
This commit is contained in:
parent
c4d8b1696c
commit
d1995dad4b
@ -7,7 +7,6 @@ require "hbc/auditor"
|
|||||||
require "hbc/cache"
|
require "hbc/cache"
|
||||||
require "hbc/cask"
|
require "hbc/cask"
|
||||||
require "hbc/cask_loader"
|
require "hbc/cask_loader"
|
||||||
require "hbc/without_source"
|
|
||||||
require "hbc/caskroom"
|
require "hbc/caskroom"
|
||||||
require "hbc/checkable"
|
require "hbc/checkable"
|
||||||
require "hbc/cli"
|
require "hbc/cli"
|
||||||
@ -24,7 +23,6 @@ require "hbc/macos"
|
|||||||
require "hbc/pkg"
|
require "hbc/pkg"
|
||||||
require "hbc/qualified_token"
|
require "hbc/qualified_token"
|
||||||
require "hbc/scopes"
|
require "hbc/scopes"
|
||||||
require "hbc/source"
|
|
||||||
require "hbc/staged"
|
require "hbc/staged"
|
||||||
require "hbc/system_command"
|
require "hbc/system_command"
|
||||||
require "hbc/topological_hash"
|
require "hbc/topological_hash"
|
||||||
@ -45,10 +43,7 @@ module Hbc
|
|||||||
Caskroom.ensure_caskroom_exists
|
Caskroom.ensure_caskroom_exists
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.load(query)
|
def self.load(ref)
|
||||||
odebug "Loading Cask definitions"
|
CaskLoader.load(ref)
|
||||||
cask = Source.for_query(query).load
|
|
||||||
cask.dumpcask
|
|
||||||
cask
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -1,44 +1,167 @@
|
|||||||
module Hbc
|
module Hbc
|
||||||
class CaskLoader
|
module CaskLoader
|
||||||
def self.load_from_file(path)
|
class FromContentLoader
|
||||||
raise CaskError, "File '#{path}' does not exist" unless path.exist?
|
def initialize(content)
|
||||||
raise CaskError, "File '#{path}' is not readable" unless path.readable?
|
@content = content
|
||||||
raise CaskError, "File '#{path}' is not a plain file" unless path.file?
|
|
||||||
|
|
||||||
token = path.basename(".rb").to_s
|
|
||||||
content = IO.read(path).force_encoding("UTF-8")
|
|
||||||
|
|
||||||
new(token, content, path).load
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.load_from_string(token, content)
|
|
||||||
new(token, content).load
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def load
|
def load
|
||||||
instance_eval(@content, __FILE__, __LINE__)
|
instance_eval(@content.force_encoding("UTF-8"), __FILE__, __LINE__)
|
||||||
rescue CaskError, StandardError, ScriptError => e
|
|
||||||
e.message.concat(" while loading '#{@token}'")
|
|
||||||
e.message.concat(" from '#{@path}'") unless @path.nil?
|
|
||||||
raise e, e.message
|
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def initialize(token, content, path = nil)
|
def cask(header_token, &block)
|
||||||
@token = token
|
Cask.new(header_token, &block)
|
||||||
@content = content
|
|
||||||
@path = path unless path.nil?
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class FromPathLoader < FromContentLoader
|
||||||
|
def self.can_load?(ref)
|
||||||
|
path = Pathname.new(ref)
|
||||||
|
path.extname == ".rb" && path.expand_path.exist?
|
||||||
|
end
|
||||||
|
|
||||||
|
attr_reader :token, :path
|
||||||
|
|
||||||
|
def initialize(path)
|
||||||
|
path = Pathname.new(path).expand_path
|
||||||
|
|
||||||
|
@token = path.basename(".rb").to_s
|
||||||
|
@path = path
|
||||||
|
end
|
||||||
|
|
||||||
|
def load
|
||||||
|
raise CaskError, "'#{@path}' does not exist." unless @path.exist?
|
||||||
|
raise CaskError, "'#{@path}' is not readable." unless @path.readable?
|
||||||
|
raise CaskError, "'#{@path}' is not a file." unless @path.file?
|
||||||
|
|
||||||
|
@content = IO.read(@path)
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def cask(header_token, &block)
|
def cask(header_token, &block)
|
||||||
raise CaskTokenDoesNotMatchError.new(@token, header_token) unless @token == header_token
|
if @token != header_token
|
||||||
|
raise CaskTokenDoesNotMatchError.new(@token, header_token)
|
||||||
|
end
|
||||||
|
|
||||||
if @path.nil?
|
Cask.new(header_token, sourcefile_path: @path, &block)
|
||||||
Cask.new(@token, &block)
|
end
|
||||||
else
|
end
|
||||||
Cask.new(@token, sourcefile_path: @path, &block)
|
|
||||||
end
|
class FromURILoader < FromPathLoader
|
||||||
|
def self.can_load?(ref)
|
||||||
|
!(ref.to_s !~ ::URI.regexp)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(url)
|
||||||
|
@url = url
|
||||||
|
uri = URI(url)
|
||||||
|
super Hbc.cache/File.basename(uri.path)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load
|
||||||
|
Hbc.cache.mkpath
|
||||||
|
FileUtils.rm_f @path
|
||||||
|
|
||||||
|
begin
|
||||||
|
ohai "Downloading #{@url}."
|
||||||
|
curl @url, "-o", @path
|
||||||
|
rescue ErrorDuringExecution
|
||||||
|
raise CaskUnavailableError, @url
|
||||||
|
end
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class FromTapLoader < FromPathLoader
|
||||||
|
def self.can_load?(ref)
|
||||||
|
!(ref.to_s !~ HOMEBREW_TAP_CASK_REGEX)
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(tapped_name)
|
||||||
|
user, repo, token = tapped_name.split("/", 3).map(&:downcase)
|
||||||
|
@tap = Tap.fetch(user, repo)
|
||||||
|
|
||||||
|
super @tap.cask_dir/"#{token}.rb"
|
||||||
|
end
|
||||||
|
|
||||||
|
def load
|
||||||
|
@tap.install unless @tap.installed?
|
||||||
|
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
class NullLoader < FromPathLoader
|
||||||
|
def self.can_load?(*)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def initialize(ref)
|
||||||
|
@token = File.basename(ref, ".rb")
|
||||||
|
super CaskLoader.default_path(@token)
|
||||||
|
end
|
||||||
|
|
||||||
|
def load
|
||||||
|
raise CaskUnavailableError, @token
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.load_from_file(path)
|
||||||
|
FromPathLoader.new(path).load
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.load_from_string(content)
|
||||||
|
FromContentLoader.new(content).load
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.path(ref)
|
||||||
|
self.for(ref).path
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.load(ref)
|
||||||
|
self.for(ref).load
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.for(ref)
|
||||||
|
[
|
||||||
|
FromURILoader,
|
||||||
|
FromTapLoader,
|
||||||
|
FromPathLoader,
|
||||||
|
].each do |loader_class|
|
||||||
|
return loader_class.new(ref) if loader_class.can_load?(ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
if FromPathLoader.can_load?(default_path(ref))
|
||||||
|
return FromPathLoader.new(default_path(ref))
|
||||||
|
end
|
||||||
|
|
||||||
|
possible_tap_casks = tap_paths(ref)
|
||||||
|
if possible_tap_casks.count == 1
|
||||||
|
possible_tap_cask = possible_tap_casks.first
|
||||||
|
return FromPathLoader.new(possible_tap_cask)
|
||||||
|
end
|
||||||
|
|
||||||
|
possible_installed_cask = Cask.new(ref)
|
||||||
|
if possible_installed_cask.installed?
|
||||||
|
return FromPathLoader.new(possible_installed_cask.installed_caskfile)
|
||||||
|
end
|
||||||
|
|
||||||
|
NullLoader.new(ref)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.default_path(token)
|
||||||
|
Hbc.default_tap.cask_dir/"#{token.to_s.downcase}.rb"
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.tap_paths(token)
|
||||||
|
Tap.map { |t| t.cask_dir/"#{token.to_s.downcase}.rb" }
|
||||||
|
.select(&:exist?)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
@ -126,15 +126,14 @@ module Hbc
|
|||||||
end
|
end
|
||||||
|
|
||||||
if token_with_tap
|
if token_with_tap
|
||||||
user, repo, token = token_with_tap.split("/")
|
user, repo, token = token_with_tap.split("/", 3)
|
||||||
tap = Tap.fetch(user, repo)
|
tap = Tap.fetch(user, repo)
|
||||||
else
|
else
|
||||||
token = query_without_extension
|
token = query_without_extension
|
||||||
tap = Hbc.default_tap
|
tap = Hbc.default_tap
|
||||||
end
|
end
|
||||||
|
|
||||||
return query_path if tap.cask_dir.nil?
|
CaskLoader.default_path(token)
|
||||||
tap.cask_dir.join("#{token}.rb")
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def tcc_db
|
def tcc_db
|
||||||
|
@ -11,15 +11,15 @@ module Hbc
|
|||||||
end
|
end
|
||||||
|
|
||||||
def all_tapped_cask_dirs
|
def all_tapped_cask_dirs
|
||||||
Tap.map(&:cask_dir).compact
|
Tap.map(&:cask_dir).select(&:directory?)
|
||||||
end
|
end
|
||||||
|
|
||||||
def all_tokens
|
def all_tokens
|
||||||
Tap.map do |t|
|
Tap.flat_map do |t|
|
||||||
t.cask_files.map do |p|
|
t.cask_files.map do |p|
|
||||||
"#{t.name}/#{File.basename(p, ".rb")}"
|
"#{t.name}/#{File.basename(p, ".rb")}"
|
||||||
end
|
end
|
||||||
end.flatten
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def installed
|
def installed
|
||||||
|
@ -1,37 +0,0 @@
|
|||||||
require "hbc/source/gone"
|
|
||||||
require "hbc/source/path_slash_required"
|
|
||||||
require "hbc/source/path_slash_optional"
|
|
||||||
require "hbc/source/tapped_qualified"
|
|
||||||
require "hbc/source/untapped_qualified"
|
|
||||||
require "hbc/source/tapped"
|
|
||||||
require "hbc/source/uri"
|
|
||||||
|
|
||||||
module Hbc
|
|
||||||
module Source
|
|
||||||
def self.sources
|
|
||||||
[
|
|
||||||
URI,
|
|
||||||
PathSlashRequired,
|
|
||||||
TappedQualified,
|
|
||||||
UntappedQualified,
|
|
||||||
Tapped,
|
|
||||||
PathSlashOptional,
|
|
||||||
Gone,
|
|
||||||
]
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.for_query(query)
|
|
||||||
odebug "Translating '#{query}' into a valid Cask source"
|
|
||||||
raise CaskUnavailableError, query if query.to_s =~ /^\s*$/
|
|
||||||
source = sources.find do |s|
|
|
||||||
odebug "Testing source class #{s}"
|
|
||||||
s.me?(query)
|
|
||||||
end
|
|
||||||
raise CaskUnavailableError, query unless source
|
|
||||||
odebug "Success! Using source class #{source}"
|
|
||||||
resolved_cask_source = source.new(query)
|
|
||||||
odebug "Resolved Cask URI or file source to '#{resolved_cask_source}'"
|
|
||||||
resolved_cask_source
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,23 +0,0 @@
|
|||||||
module Hbc
|
|
||||||
module Source
|
|
||||||
class Gone
|
|
||||||
def self.me?(query)
|
|
||||||
WithoutSource.new(query).installed?
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :query
|
|
||||||
|
|
||||||
def initialize(query)
|
|
||||||
@query = query
|
|
||||||
end
|
|
||||||
|
|
||||||
def load
|
|
||||||
WithoutSource.new(query)
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
""
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,29 +0,0 @@
|
|||||||
require "rubygems"
|
|
||||||
require "hbc/cask_loader"
|
|
||||||
|
|
||||||
module Hbc
|
|
||||||
module Source
|
|
||||||
class PathBase
|
|
||||||
# derived classes must define method self.me?
|
|
||||||
|
|
||||||
def self.path_for_query(query)
|
|
||||||
Pathname.new(query).sub(/(\.rb)?$/, ".rb")
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :path
|
|
||||||
|
|
||||||
def initialize(path)
|
|
||||||
@path = Pathname.new(path).expand_path
|
|
||||||
end
|
|
||||||
|
|
||||||
def load
|
|
||||||
CaskLoader.load_from_file(@path)
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
# stringify to fully-resolved location
|
|
||||||
@path.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,12 +0,0 @@
|
|||||||
require "hbc/source/path_base"
|
|
||||||
|
|
||||||
module Hbc
|
|
||||||
module Source
|
|
||||||
class PathSlashOptional < PathBase
|
|
||||||
def self.me?(query)
|
|
||||||
path = path_for_query(query)
|
|
||||||
path.exist?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,12 +0,0 @@
|
|||||||
require "hbc/source/path_base"
|
|
||||||
|
|
||||||
module Hbc
|
|
||||||
module Source
|
|
||||||
class PathSlashRequired < PathBase
|
|
||||||
def self.me?(query)
|
|
||||||
path = path_for_query(query)
|
|
||||||
path.to_s.include?("/") && path.exist?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,24 +0,0 @@
|
|||||||
module Hbc
|
|
||||||
module Source
|
|
||||||
class Tapped
|
|
||||||
def self.me?(query)
|
|
||||||
Hbc.path(query).exist?
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :token
|
|
||||||
|
|
||||||
def initialize(token)
|
|
||||||
@token = token
|
|
||||||
end
|
|
||||||
|
|
||||||
def load
|
|
||||||
PathSlashOptional.new(Hbc.path(token)).load
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
# stringify to fully-resolved location
|
|
||||||
Hbc.path(token).expand_path.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,21 +0,0 @@
|
|||||||
require "hbc/source/tapped"
|
|
||||||
|
|
||||||
module Hbc
|
|
||||||
module Source
|
|
||||||
class TappedQualified < Tapped
|
|
||||||
def self.me?(query)
|
|
||||||
return if (tap = tap_for_query(query)).nil?
|
|
||||||
|
|
||||||
tap.installed? && Hbc.path(query).exist?
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.tap_for_query(query)
|
|
||||||
qualified_token = QualifiedToken.parse(query)
|
|
||||||
return if qualified_token.nil?
|
|
||||||
|
|
||||||
user, repo = qualified_token[0..1]
|
|
||||||
Tap.fetch(user, repo)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,14 +0,0 @@
|
|||||||
require "hbc/source/tapped_qualified"
|
|
||||||
|
|
||||||
module Hbc
|
|
||||||
module Source
|
|
||||||
class UntappedQualified < TappedQualified
|
|
||||||
def self.me?(query)
|
|
||||||
return if (tap = tap_for_query(query)).nil?
|
|
||||||
|
|
||||||
tap.install
|
|
||||||
tap.installed? && Hbc.path(query).exist?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,32 +0,0 @@
|
|||||||
module Hbc
|
|
||||||
module Source
|
|
||||||
class URI
|
|
||||||
def self.me?(query)
|
|
||||||
!(query.to_s =~ ::URI.regexp).nil?
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_reader :uri
|
|
||||||
|
|
||||||
def initialize(uri)
|
|
||||||
@uri = uri
|
|
||||||
end
|
|
||||||
|
|
||||||
def load
|
|
||||||
Hbc.cache.mkpath
|
|
||||||
path = Hbc.cache.join(File.basename(uri))
|
|
||||||
ohai "Downloading #{uri}"
|
|
||||||
odebug "Download target -> #{path}"
|
|
||||||
begin
|
|
||||||
curl(uri, "-o", path.to_s)
|
|
||||||
rescue ErrorDuringExecution
|
|
||||||
raise CaskUnavailableError, uri
|
|
||||||
end
|
|
||||||
PathSlashOptional.new(path).load
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
uri.to_s
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -1,17 +0,0 @@
|
|||||||
module Hbc
|
|
||||||
class WithoutSource < Cask
|
|
||||||
# Override from `Hbc::DSL` because we don't have a cask source file to work
|
|
||||||
# with, so we don't know the cask's `version`.
|
|
||||||
def staged_path
|
|
||||||
(caskroom_path.children - [metadata_master_container_path]).first
|
|
||||||
end
|
|
||||||
|
|
||||||
def to_s
|
|
||||||
"#{token} (!)"
|
|
||||||
end
|
|
||||||
|
|
||||||
def installed?
|
|
||||||
caskroom_path.exist?
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
@ -19,6 +19,11 @@ class String
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def cask
|
||||||
|
$LOAD_PATH.unshift("#{HOMEBREW_LIBRARY_PATH}/cask/lib")
|
||||||
|
require "hbc"
|
||||||
|
end
|
||||||
|
|
||||||
module Homebrew
|
module Homebrew
|
||||||
module_function
|
module_function
|
||||||
|
|
||||||
|
@ -12,7 +12,11 @@ module CaskLoaderCompatibilityLayer
|
|||||||
end
|
end
|
||||||
|
|
||||||
module Hbc
|
module Hbc
|
||||||
class CaskLoader
|
module CaskLoader
|
||||||
|
class FromContentLoader; end
|
||||||
|
|
||||||
|
class FromPathLoader < FromContentLoader
|
||||||
prepend CaskLoaderCompatibilityLayer
|
prepend CaskLoaderCompatibilityLayer
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
@ -285,7 +285,7 @@ class Tap
|
|||||||
|
|
||||||
# path to the directory of all {Formula} files for this {Tap}.
|
# path to the directory of all {Formula} files for this {Tap}.
|
||||||
def formula_dir
|
def formula_dir
|
||||||
@formula_dir ||= potential_formula_dirs.detect(&:directory?)
|
@formula_dir ||= potential_formula_dirs.detect(&:directory?) || path/"Formula"
|
||||||
end
|
end
|
||||||
|
|
||||||
def potential_formula_dirs
|
def potential_formula_dirs
|
||||||
@ -294,12 +294,12 @@ class Tap
|
|||||||
|
|
||||||
# path to the directory of all {Cask} files for this {Tap}.
|
# path to the directory of all {Cask} files for this {Tap}.
|
||||||
def cask_dir
|
def cask_dir
|
||||||
@cask_dir ||= [path/"Casks"].detect(&:directory?)
|
@cask_dir ||= path/"Casks"
|
||||||
end
|
end
|
||||||
|
|
||||||
# an array of all {Formula} files of this {Tap}.
|
# an array of all {Formula} files of this {Tap}.
|
||||||
def formula_files
|
def formula_files
|
||||||
@formula_files ||= if formula_dir
|
@formula_files ||= if formula_dir.directory?
|
||||||
formula_dir.children.select(&method(:formula_file?))
|
formula_dir.children.select(&method(:formula_file?))
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
@ -308,7 +308,7 @@ class Tap
|
|||||||
|
|
||||||
# an array of all {Cask} files of this {Tap}.
|
# an array of all {Cask} files of this {Tap}.
|
||||||
def cask_files
|
def cask_files
|
||||||
@cask_files ||= if cask_dir
|
@cask_files ||= if cask_dir.directory?
|
||||||
cask_dir.children.select(&method(:cask_file?))
|
cask_dir.children.select(&method(:cask_file?))
|
||||||
else
|
else
|
||||||
[]
|
[]
|
||||||
|
@ -40,23 +40,6 @@ describe Hbc::CLI::List, :cask do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe "when Casks have been renamed" do
|
|
||||||
let(:caskroom_path) { Hbc.caskroom.join("ive-been-renamed") }
|
|
||||||
let(:staged_path) { caskroom_path.join("latest") }
|
|
||||||
|
|
||||||
before do
|
|
||||||
staged_path.mkpath
|
|
||||||
end
|
|
||||||
|
|
||||||
it "lists installed Casks without backing ruby files (due to renames or otherwise)" do
|
|
||||||
expect {
|
|
||||||
Hbc::CLI::List.run
|
|
||||||
}.to output(<<-EOS.undent).to_stdout
|
|
||||||
ive-been-renamed (!)
|
|
||||||
EOS
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe "given a set of installed Casks" do
|
describe "given a set of installed Casks" do
|
||||||
let(:caffeine) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/local-caffeine.rb") }
|
let(:caffeine) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/local-caffeine.rb") }
|
||||||
let(:transmission) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/local-transmission.rb") }
|
let(:transmission) { Hbc::CaskLoader.load_from_file(TEST_FIXTURE_DIR/"cask/Casks/local-transmission.rb") }
|
||||||
|
Loading…
x
Reference in New Issue
Block a user