Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load internal cask json v3 #16896

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
43 changes: 35 additions & 8 deletions Library/Homebrew/api/cask.rb
Expand Up @@ -42,17 +42,22 @@ def self.source_download(cask)

sig { returns(T::Boolean) }
def self.download_and_cache_data!
json_casks, updated = Homebrew::API.fetch_json_api_file "cask.jws.json"
if Homebrew::API.internal_json_v3?
json_casks, updated = Homebrew::API.fetch_json_api_file "internal/v3/homebrew-cask.jws.json"
overwrite_cache! T.cast(json_casks, T::Hash[String, T.untyped])
else
json_casks, updated = Homebrew::API.fetch_json_api_file "cask.jws.json"

cache["renames"] = {}
cache["casks"] = json_casks.to_h do |json_cask|
token = json_cask["token"]
cache["renames"] = {}
cache["casks"] = json_casks.to_h do |json_cask|
token = json_cask["token"]

json_cask.fetch("old_tokens", []).each do |old_token|
cache["renames"][old_token] = token
end
json_cask.fetch("old_tokens", []).each do |old_token|
cache["renames"][old_token] = token
end

[token, json_cask.except("token")]
[token, json_cask.except("token")]
end
end

updated
Expand All @@ -79,6 +84,28 @@ def self.all_renames
cache.fetch("renames")
end

sig { returns(Hash) }
def self.tap_migrations
# Not sure that we need to reload here.
unless cache.key?("tap_migrations")
json_updated = download_and_cache_data!
write_names(regenerate: json_updated)
end

cache["tap_migrations"]
end

sig { returns(String) }
def self.tap_git_head
# Note sure we need to reload here.
unless cache.key?("tap_git_head")
json_updated = download_and_cache_data!
write_names(regenerate: json_updated)
end

cache["tap_git_head"]
end

sig { params(regenerate: T::Boolean).void }
def self.write_names(regenerate: false)
download_and_cache_data! unless cache.key?("casks")
Expand Down
11 changes: 4 additions & 7 deletions Library/Homebrew/cask/cask.rb
Expand Up @@ -139,7 +139,7 @@
# The caskfile is needed during installation when there are
# `*flight` blocks or the cask has multiple languages
def caskfile_only?
languages.any? || artifacts.any?(Artifact::AbstractFlightBlock)
@caskfile_only || languages.any? || artifacts.any?(Artifact::AbstractFlightBlock)

Check warning on line 142 in Library/Homebrew/cask/cask.rb

View check run for this annotation

Codecov / codecov/patch

Library/Homebrew/cask/cask.rb#L142

Added line #L142 was not covered by tests
end

sig { returns(T.nilable(Time)) }
Expand Down Expand Up @@ -292,15 +292,13 @@
def populate_from_api!(json_cask)
raise ArgumentError, "Expected cask to be loaded from the API" unless loaded_from_api?

@caskfile_only = json_cask[:caskfile_only]
@languages = json_cask.fetch(:languages, [])
@tap_git_head = json_cask.fetch(:tap_git_head, "HEAD")

@ruby_source_path = json_cask[:ruby_source_path]

# TODO: Clean this up when we deprecate the current JSON API and move to the internal JSON v3.
ruby_source_sha256 = json_cask.dig(:ruby_source_checksum, :sha256)
ruby_source_sha256 ||= json_cask[:ruby_source_sha256]
@ruby_source_checksum = { "sha256" => ruby_source_sha256 }
@ruby_source_checksum = json_cask[:ruby_source_checksum] || # public JSON v2
json_cask[:ruby_source_sha256]&.then { { "sha256" => _1 } } # internal JSON v3
Comment on lines -300 to +301
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ended up removing this TODO because we won't be able to remove this going forward. We need to keep backwards compatibility with all of the JSON cask files that get stored when casks get installed from the API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to keep backwards compatibility with all of the JSON cask files that get stored when casks get installed from the API.

@apainintheneck Oof. That seems undesirable (but clearly not your fault). Could we add some sort of versioning to these files/filenames so this isn't going to have to be the case indefinitely?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we'll have to keep backwards compatibility until all of the currently supported macOS versions reach end of life. It will be a while. I'm not sure how we handle very long term deprecations like that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@apainintheneck That's not tenable given how recently this was introduced and that we have e.g. already two paths for this (writing formula files, writing JSON files). Instead, we should handle when this JSON file is in an invalid format and act the same as if it were missing entirely (which is something that already works).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess that is another option. We don't even need to explicitly handle it being in an invalid format since as you already said we handle that already. I guess in that case it shouldn't be that big of a deal.

# load the same cask file that was used for installation, if possible
def load_installed_caskfile!
Migrator.migrate_if_needed(@cask)
installed_caskfile = @cask.installed_caskfile
if installed_caskfile&.exist?
begin
@cask = CaskLoader.load(installed_caskfile)
return
rescue CaskInvalidError
# could be caused by trying to load outdated caskfile
end
end
load_cask_from_source_api! if @cask.loaded_from_api? && @cask.caskfile_only?
# otherwise we default to the current cask
end

We already fall back to the current cask file whenever the old cask file can't be loaded. I still think backwards compatibility is cheap here though and I don't understand why we would want to break it. Breaking backwards compatibility here could potentially affect all casks installed in the last year from the API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, we definitely shouldn't do this.

What is "this", sorry?

It means that you'd only be able to load a cask for any of the commands that require that if it's currently available in a tap.

This seems reasonable but I don't care enough to argue about it any more if you feel strongly about this.

The alternative is: write a JSON file into the Caskroom in the same format whether installing from homebrew-cask, other official taps or a 3rd-party tap. Again: the status quo of "install caskfile sometimes and JSON file other times" is not a good one.

Is the compatibility code really that bad?

If it was all neatly sorted into methods or a class/classes named accordingly: no big deal. As-is: it's going to be hard to extract later.

The easiest way to avoid that branching would be to have more conservative changes to the JSON layout

Again: the more conservative the approaches to changing the JSON, the less it seems worthy of having an internal JSON and/or a v3 JSON at all.

To be clear: I think the v3 JSON is very desirable and the internal one is: we just need to do it right. This is an API format that may still be with us in 5-10 years.

I do like the idea of transforming the old JSON files into the expected format but at the same time that just seems like moving the compatibility code somewhere else in the codebase.

If it's in a single location that's easier to delete/remove/deprecate later: that's better than having it inline.

See the OS layer as an example: it makes it much easier than just having if OS.mac? scattered all over the place.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, we definitely shouldn't do this.

What is "this", sorry?

My bad. I was talking about the idea of having a reduced info cask JSON that could only be used to uninstall casks.

It means that you'd only be able to load a cask for any of the commands that require that if it's currently available in a tap.

This seems reasonable but I don't care enough to argue about it any more if you feel strongly about this.

The alternative is: write a JSON file into the Caskroom in the same format whether installing from homebrew-cask, other official taps or a 3rd-party tap. Again: the status quo of "install caskfile sometimes and JSON file other times" is not a good one.

The cask JSON doesn't support all of the features found in the cask Ruby file right now so I don't think we'll be able to do that unless we change how the Ruby file works.

Is the compatibility code really that bad?

If it was all neatly sorted into methods or a class/classes named accordingly: no big deal. As-is: it's going to be hard to extract later.

Fair point. 👍

The easiest way to avoid that branching would be to have more conservative changes to the JSON layout

Again: the more conservative the approaches to changing the JSON, the less it seems worthy of having an internal JSON and/or a v3 JSON at all.

To be clear: I think the v3 JSON is very desirable and the internal one is: we just need to do it right. This is an API format that may still be with us in 5-10 years.

It'll still be smaller and more efficient just not as pretty.

I do like the idea of transforming the old JSON files into the expected format but at the same time that just seems like moving the compatibility code somewhere else in the codebase.

If it's in a single location that's easier to delete/remove/deprecate later: that's better than having it inline.

See the OS layer as an example: it makes it much easier than just having if OS.mac? scattered all over the place.

That makes sense.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My bad. I was talking about the idea of having a reduced info cask JSON that could only be used to uninstall casks.

Gotcha. You've convinced me, thanks for taking the time to do so ❤️

The cask JSON doesn't support all of the features found in the cask Ruby file right now so I don't think we'll be able to do that unless we change how the Ruby file works.

What regressions have we caused to users through not writing cask files to the Caskroom? If there are (m)any: have there been (m)any user reports about this (given that most users have been using this for over a year)?

(implication being: few/zero user complaints about this means that this is not actually a meaningful problem for users worth blocking moving everyone to this path)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What regressions have we caused to users through not writing cask files to the Caskroom? If there are (m)any: have there been (m)any user reports about this (given that most users have been using this for over a year)?

(implication being: few/zero user complaints about this means that this is not actually a meaningful problem for users worth blocking moving everyone to this path)

This is related to the casks which are Cask::Cask#caskfile_only?. They either have languages or *flight blocks. It hasn't caused any problems because we currently sidestep the problem completely and just download the original cask file (the Ruby one) whenever we install them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, forgot about that.

We should probably figure out how to add the necessary data to JSON v3.

I know, scope creep, sorry.

end

def to_s
Expand Down Expand Up @@ -361,7 +359,6 @@
# @private
def to_internal_api_hash
api_hash = {
"token" => token,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't need to be included in the hash because it's already the key of each object. Not sure how I missed this before.

"name" => name,
"desc" => desc,
"homepage" => homepage,
Expand Down
7 changes: 6 additions & 1 deletion Library/Homebrew/cask/cask_loader.rb
Expand Up @@ -299,7 +299,12 @@ def load(config:)

json_cask = Homebrew::API.merge_variations(json_cask).deep_symbolize_keys.freeze

cask_options[:tap] = Tap.fetch(json_cask[:tap]) if json_cask[:tap].to_s.include?("/")
cask_options[:tap] = if json_cask.key?(:tap) # public JSON v2
tap_value = json_cask.fetch(:tap)
Tap.fetch(tap_value) if tap_value.to_s.include?("/")
else # internal JSON v3
CoreCaskTap.instance
end
Comment on lines -302 to +307
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is just me trying to avoid using Homebrew::API.internal_json_v3? since we'll want to remove that eventually.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it makes sense to add the tap string to the json_cask value so that the cask gets saved with more information on install. It might make sense to include renames too and there might be something else I'm missing. This a cask-specific problem since we always store installed formulae with their source files.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure either of them are really necessary for uninstalls though.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be good to figure out what the bare minimum is that we actually need stored here for e.g. uninstalls and just store that, probably as a new dedicated JSON file that bears no resemblance to public or private APIs and never store the cask file.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to work on and have to maintain another format. We should be good as long as tap is included so maybe we should just add it in the be sure about things. I'm going to think a bit more about how we want to handle loading legacy cask JSON that didn't have the tap defined.

My first thought is that if we're loading from a JSON blob or file we should just use the defined tap if it exists.When loading from the API it should probably just default to the core cask tap.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it makes sense to work on and have to maintain another format.

I don't think it makes sense to store a bunch of data here that's not actually used and reuse/tweak an existing format when we already have two different "formats" (cask file vs. JSON on disk).

Note that this has been a pretty long-running issue in Cask due to writing too much here and breaking backwards compatibility in the past causing Casks to not be able to be uninstalled.

What I'm arguing for is essentially:

  • reusing the internal JSON format for this is not a good idea and going to cause more issues in future (as it has in the past)
  • the existing format contains far too much unnecessary data which has and will continue to cause issues due to the too-large surface area here that's too easy for us to break with deprecation/disables/removals
  • having 2-3 paths for what files are stored in the Caskroom is bad when Formulae has only one
  • less of a big deal but: having how Formulae and Casks behave so differently in this way is undesirable

Genuinely I apologise that I keep stumbling on things that end up blowing up the scope here, it's just that some of this stuff has been very painful over long periods of time in Homebrew and I'd rather we did it properly if we're going to introduce a new API for this.

Genuinely appreciate all your work here and elsewhere: thank you @apainintheneck ❤️

My first thought is that if we're loading from a JSON blob or file we should just use the defined tap if it exists.When loading from the API it should probably just default to the core cask tap.

This seems reasonable.

I can't see when, with the existing code, how JSON would be used for non-core cask taps though, right? It'll always use the Cask file I'd think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what would the one file type be in the case that we decided to only support one format for installed casks? We could download the original cask file whenever we install a cask and just store that. Then, we wouldn't have to support the installed JSON file at all. Or we could generate a JSON file for every cask install and only store the JSON representation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what would the one file type be in the case that we decided to only support one format for installed casks?

A JSON file.

Or we could generate a JSON file for every cask install and only store the JSON representation.

A minimal JSON file that's a subset of internal JSON v3 containing only the information needed to e.g. uninstall a cask.


user_agent = json_cask.dig(:url_specs, :user_agent)
json_cask[:url_specs][:user_agent] = user_agent[1..].to_sym if user_agent && user_agent[0] == ":"
Expand Down
2 changes: 2 additions & 0 deletions Library/Homebrew/tap.rb
Expand Up @@ -1380,6 +1380,8 @@ def cask_renames
def tap_migrations
@tap_migrations ||= if Homebrew::EnvConfig.no_install_from_api?
super
elsif Homebrew::API.internal_json_v3?
Homebrew::API::Cask.tap_migrations
else
migrations, = Homebrew::API.fetch_json_api_file "cask_tap_migrations.jws.json",
stale_seconds: TAP_MIGRATIONS_STALE_SECONDS
Expand Down
123 changes: 123 additions & 0 deletions Library/Homebrew/test/api/internal_tap_json/cask_spec.rb
@@ -0,0 +1,123 @@
# frozen_string_literal: true

RSpec.describe "Internal Tap JSON -- Cask" do
let(:internal_tap_json) { File.read(TEST_FIXTURE_DIR/"internal_tap_json/homebrew-cask.json").chomp }
let(:tap_git_head) { "b26c1e550a8b7eed2dcd5306ea8f3da3848258b3" }

context "when generating JSON", :needs_macos do
before do
FileUtils.rm_rf CoreCaskTap.instance.path
cp_r(TEST_FIXTURE_DIR/"internal_tap_json/homebrew-cask", Tap::TAP_DIRECTORY/"homebrew")
allow(Cask::Cask).to receive(:generating_hash?).and_return(true)
end

it "creates the expected hash" do
api_hash = CoreCaskTap.instance.to_internal_api_hash
api_hash["tap_git_head"] = tap_git_head # tricky to mock

expect(JSON.pretty_generate(api_hash)).to eq(internal_tap_json)
end
end

context "when loading JSON" do
before do
ENV["HOMEBREW_INTERNAL_JSON_V3"] = "1"
ENV.delete("HOMEBREW_NO_INSTALL_FROM_API")

allow(Homebrew::API).to receive(:fetch_json_api_file)
.with("internal/v3/homebrew-cask.jws.json")
.and_return([JSON.parse(internal_tap_json), false])

# `Tap.tap_migration_oldnames` looks for renames in every
# tap so `CoreTap.tap_migrations` gets called and tries to
# fetch stuff from the API. This just avoids errors.
allow(Homebrew::API).to receive(:fetch_json_api_file)
.with("internal/v3/homebrew-core.jws.json")
.and_return([{ "tap_migrations" => {}, "formulae" => {}, "aliases" => {} }, false])

# To allow `cask_names.txt` to be written to the cache.
(HOMEBREW_CACHE/"api").mkdir

Homebrew::API::Cask.clear_cache
end

it "loads cask renames" do
expect(CoreCaskTap.instance.cask_renames).to eq({
"ankerslicer" => "ankermake",
"autodesk-fusion360" => "autodesk-fusion",
"betterdummy" => "betterdisplay",
"factor-lang" => "factor",
"smlnj-lang" => "smlnj",
})
end

it "loads tap migrations" do
expect(CoreCaskTap.instance.tap_migrations).to eq({
"azure-cli" => "homebrew/core",
"basex" => "homebrew/core",
"borgbackup" => "homebrew/core",
"chronograf" => "homebrew/core",
"consul" => "homebrew/core",
})
end

it "loads tap git head" do
expect(Homebrew::API::Cask.tap_git_head)
.to eq(tap_git_head)
end

context "when loading formulae" do
let(:factor_metadata) do
{
"token" => "factor",
"name" => %w[Factor],
"desc" => "Programming language",
"homepage" => "https://factorcode.org/",
"version" => "0.99",
"ruby_source_path" => "Casks/f/factor.rb",
"ruby_source_checksum" => {
"sha256" => "a0dabe24c67269e5310b47639cf32e74f49959ba1be454b2c072805b1f04c7e5",
},
}
end

let(:smlnj_metadata) do
{
"token" => "smlnj",
"name" => ["Standard ML of New Jersey"],
"desc" => "Compiler for the Standard ML '97 programming language",
"homepage" => "https://www.smlnj.org/",
"version" => "110.99.4",
"ruby_source_path" => "Casks/s/smlnj.rb",
"ruby_source_checksum" => {
"sha256" => "d47f46a88248272314a501741460d42a8c731030912a83ef58d3c7fd1e90034d",
},
}
end

it "loads factor" do
factor = Cask::CaskLoader.load("factor")
expect(factor.to_h).to include(factor_metadata)
expect(factor.sha256).to eq("8a7968b873b5e87c83b5d0f5ddb4d3d76a2460f5e5c14edac6b18fe5957bd7d6")
expect(factor.url.to_s).to eq("https://downloads.factorcode.org/releases/0.99/factor-macosx-x86-64-0.99.dmg")
end

it "loads factor from rename" do
factor = Cask::CaskLoader.load("factor-lang")
expect(factor.to_h).to include(**factor_metadata)
end

it "loads smlnj" do
smlnj = Cask::CaskLoader.load("smlnj")
expect(smlnj.to_h).to include(**smlnj_metadata)
expect(smlnj.sha256).to eq("2bf858017b8ba43a70b30527290ed9fbbc81d9eaac1abeba62469d95392019a3")
expect(smlnj.url.to_s).to eq("http://smlnj.cs.uchicago.edu/dist/working/110.99.4/smlnj-amd64-110.99.4.pkg")
end

it "loads smlnj from rename" do
smlnj = Cask::CaskLoader.load("smlnj-lang")
expect(smlnj.to_h).to include(**smlnj_metadata)
end
end
end
end
16 changes: 6 additions & 10 deletions Library/Homebrew/test/api/internal_tap_json/formula_spec.rb
Expand Up @@ -9,10 +9,10 @@
cp_r(TEST_FIXTURE_DIR/"internal_tap_json/homebrew-core", Tap::TAP_DIRECTORY/"homebrew")

# NOTE: Symlinks can't be copied recursively so we create them manually here.
(Tap::TAP_DIRECTORY/"homebrew/homebrew-core").tap do |core_tap|
mkdir(core_tap/"Aliases")
ln_s(core_tap/"Formula/f/fennel.rb", core_tap/"Aliases/fennel-lang")
ln_s(core_tap/"Formula/p/ponyc.rb", core_tap/"Aliases/ponyc-lang")
CoreTap.instance.path.tap do |core_tap_path|
mkdir(core_tap_path/"Aliases")
ln_s(core_tap_path/"Formula/f/fennel.rb", core_tap_path/"Aliases/fennel-lang")
ln_s(core_tap_path/"Formula/p/ponyc.rb", core_tap_path/"Aliases/ponyc-lang")
end
end

Expand All @@ -37,19 +37,15 @@
# tap so `CoreCaskTap.tap_migrations` gets called and tries to
# fetch stuff from the API. This just avoids errors.
allow(Homebrew::API).to receive(:fetch_json_api_file)
.with("cask_tap_migrations.jws.json", anything)
.and_return([{}, false])
.with("internal/v3/homebrew-cask.jws.json")
.and_return([{ "tap_migrations" => {}, "casks" => {} }, false])

# To allow `formula_names.txt` to be written to the cache.
(HOMEBREW_CACHE/"api").mkdir

Homebrew::API::Formula.clear_cache
end

after do
Homebrew::API::Formula.clear_cache
end
Comment on lines -49 to -51
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already clear all caches after each test.


it "loads tap aliases" do
expect(CoreTap.instance.alias_table).to eq({
"fennel-lang" => "fennel",
Expand Down
@@ -0,0 +1,76 @@
{
"tap_git_head": "b26c1e550a8b7eed2dcd5306ea8f3da3848258b3",
"renames": {
"ankerslicer": "ankermake",
"autodesk-fusion360": "autodesk-fusion",
"betterdummy": "betterdisplay",
"factor-lang": "factor",
"smlnj-lang": "smlnj"
},
"tap_migrations": {
"azure-cli": "homebrew/core",
"basex": "homebrew/core",
"borgbackup": "homebrew/core",
"chronograf": "homebrew/core",
"consul": "homebrew/core"
},
"casks": {
"factor": {
"name": [
"Factor"
],
"desc": "Programming language",
"homepage": "https://factorcode.org/",
"url": "https://downloads.factorcode.org/releases/0.99/factor-macosx-x86-64-0.99.dmg",
"version": "0.99",
"sha256": "8a7968b873b5e87c83b5d0f5ddb4d3d76a2460f5e5c14edac6b18fe5957bd7d6",
"artifacts": [
{
"suite": [
"factor"
]
}
],
"ruby_source_path": "Casks/f/factor.rb",
"ruby_source_sha256": "a0dabe24c67269e5310b47639cf32e74f49959ba1be454b2c072805b1f04c7e5",
"caveats": "To use factor, you may need to add the $APPDIR/factor directory\nto your PATH environment variable, e.g. (for Bash shell):\n export PATH=$APPDIR/factor:\"$PATH\"\n"
},
"smlnj": {
"name": [
"Standard ML of New Jersey"
],
"desc": "Compiler for the Standard ML '97 programming language",
"homepage": "https://www.smlnj.org/",
"url": "http://smlnj.cs.uchicago.edu/dist/working/110.99.4/smlnj-amd64-110.99.4.pkg",
"version": "110.99.4",
"sha256": "2bf858017b8ba43a70b30527290ed9fbbc81d9eaac1abeba62469d95392019a3",
"artifacts": [
{
"uninstall": [
{
"pkgutil": "org.smlnj.amd64.pkg"
}
]
},
{
"pkg": [
"smlnj-amd64-110.99.4.pkg"
]
},
{
"zap": [
{
"delete": "/usr/local/smlnj"
}
]
}
],
"ruby_source_path": "Casks/s/smlnj.rb",
"ruby_source_sha256": "d47f46a88248272314a501741460d42a8c731030912a83ef58d3c7fd1e90034d",
"url_specs": {
"verified": "smlnj.cs.uchicago.edu/"
},
"caveats": "To use smlnj, you may need to add the /usr/local/smlnj/bin directory\nto your PATH environment variable, e.g. (for Bash shell):\n export PATH=/usr/local/smlnj/bin:\"$PATH\"\n"
}
}
}
@@ -0,0 +1,20 @@
cask "factor" do
version "0.99"
sha256 "8a7968b873b5e87c83b5d0f5ddb4d3d76a2460f5e5c14edac6b18fe5957bd7d6"

url "https://downloads.factorcode.org/releases/#{version}/factor-macosx-x86-64-#{version}.dmg"
name "Factor"
desc "Programming language"
homepage "https://factorcode.org/"

livecheck do
url "https://downloads.factorcode.org/releases/"
regex(%r{href=.*?(\d+(?:\.\d+)+)/}i)
end

suite "factor"

caveats do
path_environment_variable "#{appdir}/factor"
end
end