diff --git a/Library/Homebrew/brew.rb b/Library/Homebrew/brew.rb index 15c1ee4ffb..d4e348c3f0 100644 --- a/Library/Homebrew/brew.rb +++ b/Library/Homebrew/brew.rb @@ -51,6 +51,9 @@ begin ENV["PATH"] += "#{File::PATH_SEPARATOR}#{tap_cmd_dir}" end + # Add cask commands to PATH. + ENV["PATH"] += "#{File::PATH_SEPARATOR}#{HOMEBREW_LIBRARY}/Homebrew/cask/cmd" + # Add SCM wrappers. ENV["PATH"] += "#{File::PATH_SEPARATOR}#{HOMEBREW_SHIMS_PATH}/scm" diff --git a/Library/Homebrew/cask/.editorconfig b/Library/Homebrew/cask/.editorconfig new file mode 100644 index 0000000000..c6c8b36219 --- /dev/null +++ b/Library/Homebrew/cask/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 2 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/Library/Homebrew/cask/.gitattributes b/Library/Homebrew/cask/.gitattributes new file mode 100644 index 0000000000..176a458f94 --- /dev/null +++ b/Library/Homebrew/cask/.gitattributes @@ -0,0 +1 @@ +* text=auto diff --git a/Library/Homebrew/cask/.github/ISSUE_TEMPLATE.md b/Library/Homebrew/cask/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000000..b20c08e86c --- /dev/null +++ b/Library/Homebrew/cask/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,9 @@ +Before writing your issue, check our instructions for [reporting bugs](https://github.com/caskroom/homebrew-cask#reporting-bugs) or [making requests](https://github.com/caskroom/homebrew-cask#requests), as appropriate. Those will walk you through the process. + +If none of those is appropriate, then **delete all this pre-inserted template text** and tell us your issue in as much detail as possible. + +Please note that if it is apparent you ignored the instructions for reporting issues, your issue may be closed without review. When the guide isn‘t followed we get the same issues over and over. Having to repeatedly deal with the same solved and documented problems leads to maintainer burnout and a lot of wasted hours that could instead have been spent improving Homebrew-Cask itself and fixing real bugs. + +If the guide itself was unclear, open *first* an issue or pull request stating what you found was confusing *and only then* your other issue. + +Thank you for taking the time to make a correct report. diff --git a/Library/Homebrew/cask/.github/PULL_REQUEST_TEMPLATE.md b/Library/Homebrew/cask/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000000..8cafcc37d4 --- /dev/null +++ b/Library/Homebrew/cask/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,27 @@ +##### Instructions + +- Look for and complete the section relevant to your submission. Delete the others, including these `Instructions`. +- `{{cask_file}}` represents the cask file you’re submitting/editing (if applicable). +- If there’s a checkbox you can’t complete for any reason, that’s OK. Just explain in detail why you weren’t able to do so. + +### Changes to a cask +#### Editing an existing cask + +- [ ] Commit message includes cask’s name (and new version, if applicable). +- [ ] `brew cask audit --download {{cask_file}}` is error-free. +- [ ] `brew cask style --fix {{cask_file}}` left no offenses. + +#### Adding a new cask + +- [ ] Checked there aren’t open [pull requests](https://github.com/caskroom/homebrew-cask/pulls) for the same cask. +- [ ] Checked there aren’t closed [issues](https://github.com/caskroom/homebrew-cask/issues) where that cask was already refused. +- [ ] When naming the cask, followed the [token reference](https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/token_reference.md). +- [ ] Commit message includes cask’s name. +- [ ] `brew cask audit --download {{cask_file}}` is error-free. +- [ ] `brew cask style --fix {{cask_file}}` left no offenses. +- [ ] `brew cask install {{cask_file}}` worked successfully. +- [ ] `brew cask uninstall {{cask_file}}` worked successfully. + +### Changes to the core + +- [ ] Followed [hacking.md](https://github.com/caskroom/homebrew-cask/blob/master/doc/development/hacking.md). diff --git a/Library/Homebrew/cask/.gitignore b/Library/Homebrew/cask/.gitignore new file mode 100644 index 0000000000..73d2c55da8 --- /dev/null +++ b/Library/Homebrew/cask/.gitignore @@ -0,0 +1,10 @@ +# toplevel +/.bundle/ +/bin/ +/vendor/ + +# anywhere in the tree +*~ +.DS_Store +.ruby-version +coverage diff --git a/Library/Homebrew/cask/.rspec b/Library/Homebrew/cask/.rspec new file mode 100644 index 0000000000..83e16f8044 --- /dev/null +++ b/Library/Homebrew/cask/.rspec @@ -0,0 +1,2 @@ +--color +--require spec_helper diff --git a/Library/Homebrew/cask/.rubocop.yml b/Library/Homebrew/cask/.rubocop.yml new file mode 100644 index 0000000000..31a2a193de --- /dev/null +++ b/Library/Homebrew/cask/.rubocop.yml @@ -0,0 +1,89 @@ +require: 'rubocop-cask' + +AllCops: + TargetRubyVersion: 2.0 + Exclude: + - '**/Casks/**/*' + - 'developer/**/*' + - '**/vendor/**/*' + +Metrics/AbcSize: + Enabled: false + +Metrics/ClassLength: + Enabled: false + +Metrics/CyclomaticComplexity: + Enabled: false + +Metrics/MethodLength: + Enabled: false + +Metrics/PerceivedComplexity: + Enabled: false + +Metrics/ModuleLength: + CountComments: false + Exclude: + - 'lib/hbc/locations.rb' + - 'lib/hbc/macos.rb' + - 'lib/hbc/utils.rb' + +Style/BlockDelimiters: + EnforcedStyle: semantic + FunctionalMethods: + - expect + - let + - let! + - subject + - watch + - inject + - map + - map! + - collect + - collect! + - reject + - reject! + - delete_if + - with_object + ProceduralMethods: + - after + - at_exit + - before + - benchmark + - bm + - bmbm + - capture_io + - capture_output + - capture_subprocess_io + - chdir + - context + - create + - each_with_object + - fork + - measure + - new + - open + - realtime + - shutup + - tap + - each + - reverse_each + IgnoredMethods: + - it + - its + - lambda + - proc + + +Style/ClassAndModuleChildren: + EnforcedStyle: compact + +Style/PredicateName: + NameWhitelist: is_32_bit?, is_64_bit? + +Style/RaiseArgs: + EnforcedStyle: exploded + +Style/StringLiterals: + EnforcedStyle: double_quotes diff --git a/Library/Homebrew/cask/.simplecov b/Library/Homebrew/cask/.simplecov new file mode 100644 index 0000000000..f152ccc91b --- /dev/null +++ b/Library/Homebrew/cask/.simplecov @@ -0,0 +1,8 @@ +SimpleCov.start do + add_filter 'bin/' + add_filter 'Casks/' + add_filter 'developer/' + add_filter 'doc/' + add_filter 'spec/' + add_filter 'test/' +end diff --git a/Library/Homebrew/cask/.travis.yml b/Library/Homebrew/cask/.travis.yml new file mode 100644 index 0000000000..5bd5fb569c --- /dev/null +++ b/Library/Homebrew/cask/.travis.yml @@ -0,0 +1,57 @@ +language: ruby + +sudo: false + +env: + global: + - LANG=en_US.UTF-8 + - LANGUAGE=en_US.UTF-8 + - LC_ALL=en_US.UTF-8 + +matrix: + include: + - env: OSX=10.11 HOMEBREW_RUBY=2.0.0 + os: osx + osx_image: xcode7.3 + rvm: system + - env: OSX=10.10 HOMEBREW_RUBY=2.0.0 + os: osx + osx_image: xcode7.1 + rvm: system + fast_finish: true + +branches: + only: + - master + +cache: + directories: + - $HOME/.gem + +before_install: + - . ci/travis/before_install.sh + +install: + - . ci/travis/install.sh + +before_script: + - . ci/travis/before_script.sh + +script: + - . ci/travis/script.sh + +notifications: + email: false + irc: + channels: + - "chat.freenode.net#homebrew-cask" + template: + - "(%{repository_name}) %{build_number}: %{branch}@%{commit} %{author} -> %{message} %{build_url}" + use_notice: true + skip_join: true + webhooks: + urls: + - "https://webhooks.gitter.im/e/712d699360b239db14a5" + on_success: change + on_failure: always + on_start: never diff --git a/Library/Homebrew/cask/CONDUCT.md b/Library/Homebrew/cask/CONDUCT.md new file mode 100644 index 0000000000..504cf1e590 --- /dev/null +++ b/Library/Homebrew/cask/CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or +advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and +appropriate to the circumstances. The project team is obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/Library/Homebrew/cask/CONTRIBUTING.md b/Library/Homebrew/cask/CONTRIBUTING.md new file mode 100644 index 0000000000..195522ab4e --- /dev/null +++ b/Library/Homebrew/cask/CONTRIBUTING.md @@ -0,0 +1,71 @@ +# How To Contribute + +:+1::tada: First off, thanks for taking the time to contribute! :tada::+1: + +* [Updating a Cask](#updating-a-cask) +* [Getting Set Up To Contribute](#getting-set-up-to-contribute) +* [Adding a Cask](#adding-a-cask) +* [Style guide](#style-guide) +* [Reporting Bugs](README.md#reporting-bugs) + + +## Updating a Cask + +Notice an application that's out-of-date in Homebrew-Cask? In most cases, it's very simple to update it. We have a [script](https://github.com/vitorgalvao/tiny-scripts/blob/master/cask-repair) that will ask for the new version number, and take care of updating the Cask file and submitting a pull request to us: + +```bash +# install and setup script - only needed once +brew install vitorgalvao/tiny-scripts/cask-repair +cask-repair --help + +# fork homebrew-cask to your account - only needed once +cd "$(brew --repository)/Library/Taps/caskroom/homebrew-cask/Casks" +hub fork + +# use to update +outdated_cask='' +github_user='' +cd "$(brew --repository)/Library/Taps/caskroom/homebrew-cask/Casks" + +cask-repair --pull origin --push $github_user $outdated_cask +``` + +If there is a more complicated change, or there is a case where `cask-repair` fails, you can also follow the steps in [Adding a Cask](doc/development/adding_a_cask.md) to do the same thing manually. Remember to update the `version` and `shasum` values, as well as the appcast [`checkpoint`](doc/cask_language_reference/stanzas/appcast.md), if there is one. + + +## Getting Set Up To Contribute + +For manual updates, you'll need to fork the repository and add your copy as a remote (can also be done with `hub fork`). + +1. Fork the repository in GitHub with the `Fork` button. + +2. Add your GitHub fork as a remote for your homebrew-cask Tap: + +```bash +$ github_user='' +$ cd "$(brew --repository)"/Library/Taps/caskroom/homebrew-cask +$ git remote add "$github_user" "https://github.com/$github_user/homebrew-cask" +``` + +3. Switch to a new branch (ie. `new-feature`), and work from there: `git checkout -b new-feature` + + +## Adding a Cask + +Notice an application that's not in Homebrew-Cask yet? Make sure it's not yet in [Homebrew-Versions](https://github.com/caskroom/homebrew-versions) (can be searched from the Github repository page) or [Homebrew](https://github.com/Homebrew/homebrew) (can be searched with `brew search`). Mac App Store apps can't be installed via Homebrew-Cask, but check out [mas](https://github.com/argon/mas) for an alternative. + +With a bit of work, you can create a Cask for it. [This document](doc/development/adding_a_cask.md) will walk you through creating a new Cask, testing it, and submitting it to us. + + +## Style guide + +Some style guidelines: + +* All Casks and code should be indented using two spaces (never tabs) +* There should not be any extraneous comments - the only comments that should be used are the ones explicitly defined in the [Cask Language Reference](doc/cask_language_reference) +* The stanza order and position of newlines is important to make things easier (See [Stanza order](doc/cask_language_reference/#stanza-order)) +* Use string manipulations to improve the maintainability of your Cask (See [`version` methods](doc/cask_language_reference/stanzas/version.md#version-methods)) +* Test your cask using `brew cask audit/style` (See [testing](doc/development/adding_a_cask.md#testing-your-new-cask)) +* Make one Pull Request per Cask change +* Squash commits after updating a Pull Request +* Use descriptive commit messages - mention app name and version (ie. `Upgrade Transmission.app to v2.82`) diff --git a/Library/Homebrew/cask/Gemfile b/Library/Homebrew/cask/Gemfile new file mode 100644 index 0000000000..cc1771d116 --- /dev/null +++ b/Library/Homebrew/cask/Gemfile @@ -0,0 +1,26 @@ +source "https://rubygems.org" + +gem "rake" + +group :debug do + gem "pry" + gem "pry-byebug", platforms: :mri +end + +group :development do + gem "rubocop-cask", "~> 0.8.3" +end + +group :release do + gem "ronn", "0.7.3" +end + +group :test do + gem "coveralls", require: false + gem "minitest", "5.4.1" + gem "minitest-reporters" + gem "mocha", "1.1.0", require: false + gem "rspec", "~> 3.0.0" + gem "rspec-its", require: false + gem "rspec-wait", require: false +end diff --git a/Library/Homebrew/cask/Gemfile.lock b/Library/Homebrew/cask/Gemfile.lock new file mode 100644 index 0000000000..954938b17a --- /dev/null +++ b/Library/Homebrew/cask/Gemfile.lock @@ -0,0 +1,119 @@ +GEM + remote: https://rubygems.org/ + specs: + ansi (1.5.0) + ast (2.3.0) + builder (3.2.2) + byebug (8.2.1) + coderay (1.1.0) + coveralls (0.8.10) + json (~> 1.8) + rest-client (>= 1.6.8, < 2) + simplecov (~> 0.11.0) + term-ansicolor (~> 1.3) + thor (~> 0.19.1) + tins (~> 1.6.0) + diff-lcs (1.2.5) + docile (1.1.5) + domain_name (0.5.25) + unf (>= 0.0.5, < 1.0.0) + hpricot (0.8.6) + http-cookie (1.0.2) + domain_name (~> 0.5) + json (1.8.3) + metaclass (0.0.4) + method_source (0.8.2) + mime-types (2.99) + minitest (5.4.1) + minitest-reporters (1.1.7) + ansi + builder + minitest (>= 5.0) + ruby-progressbar + mocha (1.1.0) + metaclass (~> 0.0.1) + mustache (1.0.2) + netrc (0.11.0) + parser (2.3.1.2) + ast (~> 2.2) + powerpack (0.1.1) + pry (0.10.3) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) + pry-byebug (3.3.0) + byebug (~> 8.0) + pry (~> 0.10) + public_suffix (2.0.2) + rainbow (2.1.0) + rake (10.4.2) + rdiscount (2.1.8) + rest-client (1.8.0) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 3.0) + netrc (~> 0.7) + ronn (0.7.3) + hpricot (>= 0.8.2) + mustache (>= 0.7.0) + rdiscount (>= 1.5.8) + rspec (3.0.0) + rspec-core (~> 3.0.0) + rspec-expectations (~> 3.0.0) + rspec-mocks (~> 3.0.0) + rspec-core (3.0.4) + rspec-support (~> 3.0.0) + rspec-expectations (3.0.4) + diff-lcs (>= 1.2.0, < 2.0) + rspec-support (~> 3.0.0) + rspec-its (1.2.0) + rspec-core (>= 3.0.0) + rspec-expectations (>= 3.0.0) + rspec-mocks (3.0.4) + rspec-support (~> 3.0.0) + rspec-support (3.0.4) + rspec-wait (0.0.8) + rspec (>= 2.11, < 3.5) + rubocop (0.41.2) + parser (>= 2.3.1.1, < 3.0) + powerpack (~> 0.1) + rainbow (>= 1.99.1, < 3.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.0, >= 1.0.1) + rubocop-cask (0.8.3) + public_suffix (~> 2.0) + rubocop (~> 0.41.1) + ruby-progressbar (1.8.1) + simplecov (0.11.1) + docile (~> 1.1.0) + json (~> 1.8) + simplecov-html (~> 0.10.0) + simplecov-html (0.10.0) + slop (3.6.0) + term-ansicolor (1.3.2) + tins (~> 1.0) + thor (0.19.1) + tins (1.6.0) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.1) + unicode-display_width (1.1.0) + +PLATFORMS + ruby + +DEPENDENCIES + coveralls + minitest (= 5.4.1) + minitest-reporters + mocha (= 1.1.0) + pry + pry-byebug + rake + ronn (= 0.7.3) + rspec (~> 3.0.0) + rspec-its + rspec-wait + rubocop-cask (~> 0.8.3) + +BUNDLED WITH + 1.12.5 diff --git a/Library/Homebrew/cask/LICENSE b/Library/Homebrew/cask/LICENSE new file mode 100644 index 0000000000..62542ae576 --- /dev/null +++ b/Library/Homebrew/cask/LICENSE @@ -0,0 +1,23 @@ +Copyright © 2013-2016, Paul Hinze & Contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, this + list of conditions and the following disclaimer in the documentation and/or + other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Library/Homebrew/cask/README.md b/Library/Homebrew/cask/README.md new file mode 100644 index 0000000000..35fe201a5d --- /dev/null +++ b/Library/Homebrew/cask/README.md @@ -0,0 +1,76 @@ +# Homebrew-Cask + +_“To install, drag this icon…” no more!_ + +Homebrew-Cask extends [Homebrew](http://brew.sh) and brings its elegance, simplicity, and speed to the installation and management of GUI macOS applications such as Google Chrome and Adium. + +We do this by providing a friendly Homebrew-style CLI workflow for the administration of macOS applications distributed as binaries. + +It’s implemented as a `homebrew` [external command](https://github.com/Homebrew/brew/blob/master/share/doc/homebrew/External-Commands.md) called `cask`. + +[![Build Status](https://img.shields.io/travis/caskroom/homebrew-cask/master.svg)](https://travis-ci.org/caskroom/homebrew-cask) +[![Code Climate](https://img.shields.io/codeclimate/github/caskroom/homebrew-cask.svg)](https://codeclimate.com/github/caskroom/homebrew-cask) +[![Coverage Status](https://img.shields.io/coveralls/caskroom/homebrew-cask.svg)](https://coveralls.io/r/caskroom/homebrew-cask) +[![Join the chat at https://gitter.im/caskroom/homebrew-cask](https://img.shields.io/badge/gitter-join%20chat-blue.svg)](https://gitter.im/caskroom/homebrew-cask) + +## Let’s try it! + +To start using Homebrew-Cask, you just need [Homebrew](http://brew.sh/) installed. + +Installing Atom (animated gif) + +Slower, now: + +``` +$ brew cask install atom +==> Satisfying dependencies +complete +==> Downloading https://github.com/atom/atom/releases/download/v1.8.0/atom-mac.zip +######################################################################## 100.0% +==> Verifying checksum for Cask atom +==> Moving App 'Atom.app' to '/Applications/Atom.app' +==> Symlinking Binary 'apm' to '/usr/local/bin/apm' +==> Symlinking Binary 'atom.sh' to '/usr/local/bin/atom' +🍺 atom was successfully installed! +``` + +And there we have it. Atom installed with one quick command: no clicking, no dragging, no dropping. + +## Learn More + +* Find basic documentation on using Homebrew-Cask in [USAGE.md](USAGE.md) +* Want to contribute a Cask? Awesome! See [CONTRIBUTING.md](CONTRIBUTING.md) +* Want to hack on our code? Also awesome! See [hacking.md](doc/development/hacking.md) +* More project-related details and discussion are available in the [documentation](doc) + +## Reporting bugs + +Like most pieces of software, Homebrew-Cask has bugs — and we are busy fixing them! If you find a new bug tell us about it, but before you do make sure the problem isn’t simply an outdated setup on your side, by following [this guide](doc/reporting_bugs/pre_bug_report.md). + +If your issue persists, follow these instructions to the appropriate course of action: + +* [A cask fails to install](doc/reporting_bugs/a_cask_fails_to_install.md) +* [`brew cask list` shows wrong information](doc/reporting_bugs/brew_cask_list_shows_wrong_information.md) +* [`uninstall` wrongly reports cask as not installed](doc/reporting_bugs/uninstall_wrongly_reports_cask_as_not_installed.md) +* [My problem isn’t listed][bug_report_template] + +## Requests + +* Start an issue on GitHub following one of these templates: + * [Feature request][feature_request_template] + * [Cask request][cask_request_template] + +## Questions? Wanna chat? + +We’re really rather friendly! Here are the best places to talk about the project: + +* If none of the templates above is appropriate, [open an issue](https://github.com/caskroom/homebrew-cask/issues/new). +* Join us (and [caskbot](https://github.com/passcod/caskbot)) on IRC at `#homebrew-cask` on Freenode +* Join us on [Gitter](https://gitter.im/caskroom/homebrew-cask) + +## License +Code is under the [BSD 2 Clause (NetBSD) license](LICENSE) + +[bug_report_template]: https://github.com/caskroom/homebrew-cask/issues/new?title=Bug%20report%3A&body=Remember%20to%20follow%20the%20%5Bpre%20bug%20report%5D%28https%3A%2F%2Fgithub.com%2Fcaskroom%2Fhomebrew-cask%2Fblob%2Fmaster%2Fdoc%2Freporting_bugs%2Fpre_bug_report.md%29%20guide%20beforehand.%20Failure%20to%20do%20so%20might%20get%20your%20issue%20closed.%0A%0A%23%23%23%23%20Description%20of%20issue%0A%0A%5Binsert%20a%20detailed%20description%20of%20your%20issue%20here%5D%0A%0A%3Cdetails%3E%3Csummary%3EOutput%20of%20%60brew%20cask%20%3Ccommand%3E%20--verbose%60%3C%2Fsummary%3E%0A%0A%60%60%60%0A%5Bpaste%20output%20here%5D%0A%60%60%60%0A%3C%2Fdetails%3E%0A%0A%3Cdetails%3E%3Csummary%3EOutput%20of%20%60brew%20doctor%60%3C%2Fsummary%3E%0A%0A%60%60%60%0A%5Bpaste%20output%20here%5D%0A%60%60%60%0A%3C%2Fdetails%3E%0A%0A%3Cdetails%3E%3Csummary%3EOutput%20of%20%60brew%20cask%20doctor%60%3C%2Fsummary%3E%0A%0A%60%60%60%0A%5Bpaste%20output%20here%5D%0A%60%60%60%0A%3C%2Fdetails%3E%0A +[cask_request_template]: https://github.com/caskroom/homebrew-cask/issues/new?title=Cask%20request%3A&body=%23%23%23%20Cask%20details%0A%0A%28Please%20fill%20out%20as%20much%20as%20possible%29%0A%0A%2A%2AName%2A%2A%20-%0A%0A%2A%2AHomepage%2A%2A%20-%0A%0A%2A%2ALicense%2A%2A%20-%0A%0A%2A%2ADownload%20URL%2A%2A%20-%0A%0A%2A%2ADescription%2A%2A%20-%0A +[feature_request_template]: https://github.com/caskroom/homebrew-cask/issues/new?title=Feature%20request%3A&body=%23%23%23%20Description%20of%20feature%2Fenhancement%0A%0A%0A%0A%23%23%23%20Justification%0A%0A%0A%0A%23%23%23%20Example%20use%20case%0A%0A%0A%0A diff --git a/Library/Homebrew/cask/Rakefile b/Library/Homebrew/cask/Rakefile new file mode 100644 index 0000000000..a8373fa96e --- /dev/null +++ b/Library/Homebrew/cask/Rakefile @@ -0,0 +1,52 @@ +require "coveralls/rake/task" +require "rake/testtask" +require "rspec/core/rake_task" +require "rubocop/rake_task" + +homebrew_repo = `brew --repository`.chomp +$LOAD_PATH.unshift(File.expand_path("#{homebrew_repo}/Library/Homebrew")) +$LOAD_PATH.unshift(File.expand_path("../lib", __FILE__)) + +namespace :test do + Rake::TestTask.new(:minitest) do |t| + # TODO: setting the --seed here is an ugly temporary hack, to remain only + # until test-suite glitches are fixed. + ENV["TESTOPTS"] = "--seed=14830" if ENV["TRAVIS"] + t.pattern = "test/**/*_test.rb" + t.libs << "test" + end + + RSpec::Core::RakeTask.new(:rspec) + + desc "Run tests for minitest and RSpec with coverage" + task :coverage do + ENV["COVERAGE"] = "1" + Rake::Task[:test].invoke + end +end + +desc "Run tests for minitest and RSpec" +task test: ["test:minitest", "test:rspec"] + +Coveralls::RakeTask.new + +RuboCop::RakeTask.new(:rubocop) do |t| + t.options = ["--force-exclusion"] +end + +task default: [:test, :rubocop] + +desc "Open a REPL for debugging and experimentation" +task :console do + require "pry" + require "pry-byebug" + require "hbc" + ARGV.clear + Hbc.pry +end + +desc "Generate man page from Markdown source" +task :man do + sh "ronn --roff --pipe --organization=Homebrew-Cask --manual=brew-cask " \ + "doc/man_page/brew-cask.1.md > man/man1/brew-cask.1" +end diff --git a/Library/Homebrew/cask/USAGE.md b/Library/Homebrew/cask/USAGE.md new file mode 100644 index 0000000000..c8ef5eabe1 --- /dev/null +++ b/Library/Homebrew/cask/USAGE.md @@ -0,0 +1,230 @@ +# How to Use Homebrew-Cask + +## Getting Started + +First ensure you have Homebrew version `0.9.5` or higher: + +```bash +$ brew --version +0.9.5 +``` + +## Frequently Used Commands + +Homebrew-Cask is implemented as a subcommand of Homebrew. All Homebrew-Cask commands begin with `brew cask`. Homebrew-Cask has its own set of command verbs many of which are similar to Homebrew’s. The most frequently-used +commands are: + +* `search` — searches all known Casks +* `install` — installs the given Cask +* `uninstall` — uninstalls the given Cask + +## Searching for Casks + +The `brew cask search` command accepts a series of substring arguments, and returns tokens representing matching Casks. Let’s see if there’s a Cask for Google Chrome: + +```bash +$ brew cask search chrome +google-chrome +``` + +A `search` command with no search term will list all available Casks: + +```bash +$ brew cask search +# +``` + +## Installing Casks + +The command `brew cask install` accepts a Cask token as returned by `brew cask search`. Let’s try to install Google Chrome: + +```bash +$ brew cask install google-chrome +==> Downloading https://dl.google.com/chrome/mac/stable/GGRO/googlechrome.dmg +==> Moving App 'Google Chrome.app' to '/Applications/Google Chrome.app' +🍺 google-chrome was successfully installed! +``` + +## Uninstalling Casks + +Easy peasy: + +```bash +$ brew cask uninstall google-chrome +``` + +This will both uninstall the Cask and remove applications which were moved to `/Applications`. + +To uninstall all versions of a Cask, use `--force`: + +```bash +$ brew cask uninstall --force google-chrome +``` + +Note that `uninstall --force` is currently imperfect. See the man page for more information. + +## Other Commands + +* `info` — displays information about the given Cask +* `list` — with no args, lists installed Casks; given installed Casks, lists staged files +* `fetch` — downloads remote application files for the given Cask to the local cache (with `--force`, re-download even if already cached) +* `doctor` — checks for configuration issues +* `cleanup` — cleans up cached downloads (with `--outdated`, only cleans old downloads) +* `home` — opens the homepage of the given Cask; or with no arguments, the Homebrew-Cask project page +* `update` — a synonym for `brew update` +* `zap` — try to remove *all* files associated with a Cask (may include resources shared with other applications) + +The following commands are for Cask authors: + +* `audit` — verifies installability of Casks +* `cat` — dumps the given Cask to the standard output +* `create` — creates a Cask and opens it in an editor +* `edit` — edits the given Cask + +The following aliases and abbreviations are provided for convenience: + +* `ls` — `list` +* `-S` — `search` +* `rm`, `remove` — `uninstall` +* `up` — `update` +* `dr` — `doctor` + +## Tab Completion + +[Homebrew/homebrew-completions](https://github.com/Homebrew/homebrew-completions) supports `bash` and `fish` completions (only for `brew-cask` right now). Install them with: + +```bash +$ brew install homebrew/completions/brew-cask-completion +``` + +For `zsh` completion support, simply run: + +```bash +$ brew install zsh-completions +``` + +## Inspecting Installed Casks + +List all installed Casks: + +```bash +$ brew cask list +adium google-chrome onepassword +``` + +Show details about a specific Cask: + +```bash +$ brew cask info caffeine +caffeine: 1.1.1 +http://lightheadsw.com/caffeine/ +Not installed +From: https://github.com/caskroom/homebrew-cask/blob/master/Casks/caffeine.rb +==> Name +Caffeine +==> Artifacts +Caffeine.app (app) +``` + +## Updating/Upgrading Casks + +Since the Homebrew-Cask repository is a Homebrew Tap, you’ll pull down the latest Casks every time you issue the regular Homebrew command `brew update`. Currently, Homebrew-Cask cannot always detect if an application has been updated. You can force an update via the command `brew cask install --force`. We are working on improving this. + +It is generally safe to run updates from within an application. + +## Updating/Upgrading the Homebrew-Cask Tool + +Homebrew [automatically taps and keeps Homebrew-Cask updated](https://github.com/caskroom/homebrew-cask/pull/15381). `brew update` is all that is required. + +## Additional Taps (optional) + +The primary Homebrew-Cask Tap includes most of the Casks that a typical user will be interested in. There are a few additional Taps where we store different kinds of Casks. + +| Tap name | description | +| -------- | ----------- | +| [caskroom/versions](https://github.com/caskroom/homebrew-versions) | contains alternate versions of Casks (e.g. betas, nightly releases, old versions) +| [caskroom/fonts](https://github.com/caskroom/homebrew-fonts) | contains Casks that install fonts, which are kept separate so we can educate users about the different licensing landscape around font installation/usage +| [caskroom/eid](https://github.com/caskroom/homebrew-eid) | contains Casks that install electronic identity card software of various countries + +You can tap any of the above with a `brew tap` command: + +```bash +$ brew tap +``` + +after which, Casks from the new Tap will be available to `search` or `install` just like Casks from the main Tap. `brew update` will automatically keep your new Tap up to date. + +You may also specify a fully-qualified Cask token (which includes the Tap) for any `brew cask` command. This will implicitly add the Tap if you have not previously added it with `brew tap`: + +```bash +$ brew cask install caskroom/fonts/font-symbola +``` + +## Options + +`brew cask` accepts a number of options: + +* `--version`: print version and exit +* `--debug`: output debug information +* `--no-binaries`: skip symlinking executable binaries into `/usr/local/bin` +* `--require-sha`: abort installation of cask if no checksum is defined + +You can also modify the default installation locations used when issuing `brew cask install`: + +* `--caskroom=/my/path` determines where the actual applications will be located. +Default is `$(brew --repository)/Caskroom` +* `--appdir=/my/path` changes the path where the applications (above) +will be moved. Default is `/Applications`. +* `--prefpanedir=/my/path` changes the path for PreferencePanes. +Default is `~/Library/PreferencePanes` +* `--qlplugindir=/my/path` changes the path for Quicklook Plugins. +Default is `~/Library/QuickLook` +* `--fontdir=/my/path` changes the path for Fonts. +Default is `~/Library/Fonts` +* `--input_methoddir=/my/path` changes the path for Input Methods. +Default is `~/Library/Input Methods` +* `--screen_saverdir=/my/path` changes the path for Screen Savers. +Default is `~/Library/Screen Savers` + +To make these settings persistent, you might want to add the following line to your `.bash_profile` or `.zshenv`: + +```bash +# Specify your defaults in this environment variable +export HOMEBREW_CASK_OPTS="--appdir=~/Applications --caskroom=/etc/Caskroom" +``` + +Note that you still can override the environment variable `HOMEBREW_CASK_OPTS` by _explicitly_ providing options in the command line: + +```bash +# Will force the Chrome app to be moved to /Applications +# even though HOMEBREW_CASK_OPTS specified ~/Applications +$ brew cask install --appdir="/Applications" google-chrome +``` + +## Advanced Searching + +The default search algorithm is a lax substring approach, which does not use the command-line arguments exactly as given. If you need to specify a search more precisely, a single search argument enclosed in `/` characters will be taken as a Ruby regular expression: + +```bash +$ brew cask search '/^google.c[a-z]rome$/' +google-chrome +``` + +## Other Ways to Specify a Cask + +Most `brew cask` commands can accept a Cask token as an argument. As described above, the token on the command line can take the form of: + +* A token as returned by `brew cask search`, _eg_: `google-chrome`. +* A fully-qualified token which includes the Tap, _eg_: `caskroom/fonts/font-symbola`. + +`brew cask` also accepts three other forms as arguments: + +* A path to a Cask file, _eg_: `/usr/local/Library/Taps/caskroom/homebrew-cask/Casks/google-chrome.rb`. +* A `curl`-retrievable URI to a Cask file, _eg_: `https://raw.githubusercontent.com/caskroom/homebrew-cask/f25b6babcd398abf48e33af3d887b2d00de1d661/Casks/google-chrome.rb`. +* A file in the current working directory, _eg_: `my-modfied-google-chrome.rb`. Note that matching Tapped Cask tokens will be preferred over this form when there is a conflict. To force the use of a Cask file in the current directory, specify a pathname with slashes, _eg_: `./google-chrome.rb`. + +The last three forms are intended for users who wish to maintain private Casks. + +## Taps + +You can add Casks to your existing (or new) Taps: just create a directory named `Casks` inside your Tap, put your Cask files there, and everything will just work. diff --git a/Library/Homebrew/cask/cmd/brew-cask-tests.rb b/Library/Homebrew/cask/cmd/brew-cask-tests.rb new file mode 100755 index 0000000000..92ef3ddd90 --- /dev/null +++ b/Library/Homebrew/cask/cmd/brew-cask-tests.rb @@ -0,0 +1,20 @@ +require "English" + +repo_root = Pathname(__FILE__).realpath.parent.parent +repo_root.cd do + ENV["HOMEBREW_NO_ANALYTICS_THIS_RUN"] = "1" + + Homebrew.install_gem_setup_path! "bundler" + unless quiet_system("bundle", "check") + system "bundle", "install", "--path", "vendor/bundle" + end + + test_task = "test" + %w[rspec minitest coverage].each do |subtask| + next unless ARGV.flag?("--#{subtask}") + test_task = "test:#{subtask}" + end + + system "bundle", "exec", "rake", test_task + Homebrew.failed = !$CHILD_STATUS.success? +end diff --git a/Library/Homebrew/cask/cmd/brew-cask.rb b/Library/Homebrew/cask/cmd/brew-cask.rb new file mode 100755 index 0000000000..825c4cb818 --- /dev/null +++ b/Library/Homebrew/cask/cmd/brew-cask.rb @@ -0,0 +1,12 @@ +require "pathname" + +$LOAD_PATH.unshift(File.expand_path("../../lib", Pathname.new(__FILE__).realpath)) + +require "hbc" + +begin + Hbc::CLI.process(ARGV) +rescue Interrupt + puts + exit 130 +end diff --git a/Library/Homebrew/cask/developer/bin/cask-switch-https b/Library/Homebrew/cask/developer/bin/cask-switch-https new file mode 100755 index 0000000000..c7845f1eb2 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/cask-switch-https @@ -0,0 +1,190 @@ +#!/bin/bash + +set -o pipefail + +readonly program="$(basename "$0")" +skip_curl_verify=0 +verbose=0 + +syntax_error() { + echo "$program: $1" >&2 + echo "Try \`$program --help\` for more information." >&2 + exit 1 +} + +depends_on() { + formula="$1" + [[ "$#" -eq 2 ]] && cmd="$2" || cmd=$(basename "${formula}") + + if [[ ! $(which ${cmd}) ]]; then + echo -e "$(tput setaf 1) + This script depends on '${cmd}'. + If you have [Homebrew](http://brew.sh), you can install it with 'brew install ${formula}'. + $(tput sgr0)" | sed -E 's/ {6}//' >&2 + exit 1 + fi +} + +depends_on 'tsparber/tiny-scripts/curl-check-url' + +usage() { + echo " + This script changes the url, appcast and homepage stanzas to https + + After changing to https a HTTP GET request is performed to verify if the url is reachable. + If the https url is not reachable it is reverted to the previous version. + + Known Issues: If multiple url/appcast stanzas are present, all urls are changed but only + those for the current os are verified. + + If no cask name is given the current work directory is scanned with the given options. + + usage: $program [options] [] + options: + -s, --skip-verify Skip checking for a HTTP 200 Status Code using curl. + --verbose Show more verbose output. + -h, --help Show this help. + + Based on: https://github.com/vitorgalvao/tiny-scripts/blob/master/cask-repair + " | sed -E 's/^ {4}//' +} + +# available flags +while [[ "$1" ]]; do + case "$1" in + -h | --help) + usage + exit 0 + ;; + -s | --skip-verify) + skip_curl_verify=1 + ;; + --verbose) + verbose=1 + ;; + -*) + syntax_error "unrecognized option: $1" + ;; + *) + break + ;; + esac + shift +done + +# define function to check if given URL exists and is reachable using HTTPS +check_url_for_https() { + cask_url="$1" + verbose_option="" + + [[ ${verbose} -ne 0 ]] && verbose_option="-v " + + # check if the URL sends a 200 HTTP code, else abort + curl-check-url ${verbose_option} "${cask_url}" > /dev/null + exit_code=$? + + if [[ exit_code -ne 0 ]]; then + echo "curl returned ${exit_code}: FAIL for ${cask_url}" + return 1 + fi + + return 0 +} + +# define function to modify part of stanza +replace_protocol_of_stanza() { + cask_file="$1" + stanza="$2" + old_value="$3" + new_value="$4" + + sed "s|${stanza} \(['\"]\)${old_value}://|${stanza} \1${new_value}://|g" "${cask_file}" > tmpfile + mv tmpfile "${cask_file}" +} + +# define abort function, that will reset the state +finish() { + # show message + if [[ "$1" == 'abort' ]]; then + echo -e "$(tput setaf 1)$2$(tput sgr0)\n" + [[ ! -z "${cask_file}" ]] && git checkout -- "${cask_file}" + exit 1 + elif [[ "$1" == 'success' ]]; then + echo -e "$(tput setaf 2)Updated: ${cask_name} is now using HTTPS$(tput sgr0)\n" + exit 0 + fi +} + +# cleanup if aborted with ⌃C +trap 'finish abort "You aborted"' SIGINT + +# exit if not inside a 'homebrew-*/Casks' directory +casks_dir=$(pwd | perl -ne 'print m{homebrew-[^/]+/Casks}') +if [[ -z "${casks_dir}" ]]; then + echo -e "\n$(tput setaf 1)You need to be inside a '/homebrew-*/Casks' directory$(tput sgr0)\n" + exit 1 +fi + +# exit if no argument was given: Run in current directory +if [[ -z "$1" ]]; then + options="" + [[ ${skip_curl_verify} -ne 0 ]] && options+=" --skip-verify" + [[ ${verbose} -ne 0 ]] && options+=" --verbose" + + for file in *.rb; + do + "$0" ${options} ${file} + done + + exit 0 +fi + +# clean the cask's name, and check if it is valid +cask_name="$1" +[[ "${cask_name}" == *'.rb' ]] && cask_name=$(echo "${cask_name}" | sed 's|\.rb$||') +cask_file="./${cask_name}.rb" +[[ ! -f "${cask_file}" ]] && finish abort 'There is no such cask' + +# initial tasks +git checkout -- "${cask_file}" + +# check if a http url exists +cask_contains_http=$(grep "['\"]http://" "${cask_file}") +if [[ -z ${cask_contains_http} ]]; then + echo -e "Skipped ${cask_name} no http found\n" + exit 0 +fi + +updated_stanzas=0 +for stanza in url appcast homepage; do + # Check if the stanza exists + stanza_contained=$(grep "${stanza} ['\"]" "${cask_file}") + [[ -z ${stanza_contained} ]] && continue + + stanza_contains_https=$(grep "${stanza} ['\"]http://" "${cask_file}") + if [[ -z ${stanza_contains_https} ]]; then +# echo "Skipped stanza ${stanza} in ${cask_name} no http url found" + continue + fi + + replace_protocol_of_stanza ${cask_file} ${stanza} "http" "https" + + if [[ ${skip_curl_verify} -eq 0 ]]; then + check_url_for_https $(brew cask _stanza ${stanza} "${cask_name}") + else + true + fi + + if [[ $? -ne 0 ]]; then + echo "Restored original value for stanza ${stanza} as curl check failed" + replace_protocol_of_stanza ${cask_file} ${stanza} "https" "http" + else + updated_stanzas=$((updated_stanzas+1)) + fi +done + +if [[ ${updated_stanzas} -ne 0 ]]; then + finish success +else + finish abort "no updated stanzas after verify for ${cask_name}" +fi diff --git a/Library/Homebrew/cask/developer/bin/develop_brew_cask b/Library/Homebrew/cask/developer/bin/develop_brew_cask new file mode 100755 index 0000000000..6723fc978d --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/develop_brew_cask @@ -0,0 +1,199 @@ +#!/bin/bash +# +# develop_brew_cask +# +# Called via symlink as: +# production_brew_cask +# + +called_as="$(basename "$0")" + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### configurable global variables +### + +taps_subdir="Library/Taps" +cask_tap_subdir="caskroom/homebrew-cask" +dev_links=("cmd" "lib" "Casks") + +### +### functions +### + +warn () { + local message="$*" + message="${message//\\t/$'\011'}" + message="${message//\\n/$'\012'}" + message="${message%${message##*[![:space:]]}}" + printf "%s\n" "$message" 1>&2 +} + +die () { + warn "$@" + exit 1 +} + +cd_to_project_root () { + local script_dir git_root + script_dir="$(/usr/bin/dirname "$0")" + cd "$script_dir" + git_root="$(git rev-parse --show-toplevel)" + if [[ -z "$git_root" ]]; then + die "ERROR: Could not find git project root" + fi + cd "$git_root" +} + +cd_to_tap_dir () { + local taps_dir="$1" + local tap_dir="$2" + if [[ ! -d "$tap_dir" ]]; then + die "ERROR: Could not find tap dir under $taps_dir/" + fi + cd "$tap_dir" +} + +not_inside_homebrew () { + local tap_dir="$1" + local git_root="$2" + if [[ "$(/usr/bin/stat -L -f '%i' -- "$tap_dir")" -eq "$(/usr/bin/stat -L -f '%i' -- "$git_root")" ]]; then + die "\nERROR: Run this script in your private repo, not inside Homebrew.\n" + fi +} + +remove_dev_links () { + for link_name in "${dev_links[@]}"; do + remove_dev_link "$link_name" + done + printf "brew-cask is now in production mode\n" + printf "It is safe to run 'brew update' if you are in production mode for all Caskroom repos.\n" +} + +create_dev_links () { + local git_root="$1" + for link_name in "${dev_links[@]}"; do + create_dev_link "$git_root" "$link_name" + done + printf "brew-cask is now in development mode\n" + printf "Note: it is not safe to run 'brew update' while in development mode\n" +} + +remove_dev_link () { + local link_name="$1" + /bin/rm -- "$link_name" + /bin/mv -- "production_$link_name" "$link_name" +} + +create_dev_link () { + local git_root="$1" + local link_name="$2" + /bin/mv -- "$link_name" "production_$link_name" + /bin/ln -s -- "$git_root/$link_name" . +} + +### +### main +### + +_develop_brew_cask_develop_action () { + die "brew-cask is already set up for development" +} + +_develop_brew_cask_production_action () { + create_dev_links "$git_root" +} + +_production_brew_cask_develop_action () { + remove_dev_links +} + +_production_brew_cask_production_action () { + die "brew-cask is already set up for production" +} + +_main () { + local git_root brew_repository taps_dir tap_dir + + # initialization + cd_to_project_root + git_root="$(/bin/pwd)" + brew_repository="$(brew --repository)" + taps_dir="$brew_repository/$taps_subdir" + tap_dir="$taps_dir/$cask_tap_subdir" + + # sanity check + not_inside_homebrew "$tap_dir" "$git_root" + + # action + cd_to_tap_dir "$taps_dir" "$tap_dir" + if [[ -e "production_lib" ]]; then + eval "_${called_as}_develop_action" + else + eval "_${called_as}_production_action" + fi + +} + +_develop_brew_cask_usage () { + + printf "develop_brew_cask + +Symlink private repo directories into Homebrew's Cellar, so +that the 'brew cask' command will use code and Casks from +the current development branch in your private repo. + +Saves the production Homebrew directories under new names. + +You can reverse this operation with 'production_brew_cask'. + +Note: it is not safe to run 'brew update' while development +mode is in effect. + +" + +} + +_production_brew_cask_usage () { + + printf "production_brew_cask + +Undo all symlinks created by 'develop_brew_cask' so that the +'brew cask' command will use only released code and Casks +within Homebrew. + +After running this command it is safe to run 'brew update', +unless you are using similar scripts to create symlinks into +other Caskroom development repos. + +" + +} + +# ensure we're called by a valid name +case "${called_as}" in + develop_brew_cask) ;; + production_brew_cask) ;; + *) + die "ERROR: name ${called_as} not recognized" + ;; +esac + + +# process args +if [[ $1 =~ ^-+h(elp)?$ ]]; then + eval "_${called_as}_usage" + exit +fi + +# dispatch main +_main "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/find_outdated_appcasts b/Library/Homebrew/cask/developer/bin/find_outdated_appcasts new file mode 100755 index 0000000000..d6ddcf4d56 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/find_outdated_appcasts @@ -0,0 +1,121 @@ +#!/bin/bash + +readonly caskroom_online='https://github.com/caskroom' +readonly caskroom_repos_dir='/tmp/caskroom_repos' +readonly caskroom_repos=(homebrew-cask homebrew-versions homebrew-fonts homebrew-eid) +readonly curl_flags=(--silent --location --header 'User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36') +inaccessible_appcasts=() + +if [[ ! $(which 'ghi') ]] || ! security find-internet-password -s github.com -l 'ghi token' &> /dev/null; then + echo -e "$(tput setaf 1) + This script requires 'ghi' installed and configured. + If you have [Homebrew](http://brew.sh), you can install it with 'brew install ghi'. + To configure it, run 'ghi config --auth '. Your Github password will be required, but is never stored. + $(tput sgr0)" | sed -E 's/ {4}//' >&2 + exit 1 +fi + +function message { + echo "${1}" +} + +function go_to_repos_dir { + [[ ! -d "${caskroom_repos_dir}" ]] && mkdir -p "${caskroom_repos_dir}" + cd "${caskroom_repos_dir}" || exit 1 +} + +function go_to_repo_and_update { + local repo_name repo_dir casks_dir + + repo_name="${1}" + repo_dir="${caskroom_repos_dir}/${repo_name}" + casks_dir="${repo_dir}/Casks" + + if [[ ! -d "${repo_dir}" ]]; then + go_to_repos_dir + + message "Cloning ${repo_name}…" + git clone "https://github.com/caskroom/${repo_name}.git" --quiet + + cd "${casks_dir}" || exit 1 + else + cd "${casks_dir}" || exit 1 + + message "Updating ${repo_name}…" + git pull --rebase origin master --quiet + fi +} + +function open_issue { + local repo_name cask_name cask_url version appcast_url issue_number + + repo_name="${1}" + cask_name="${2}" + cask_url="${caskroom_online}/${repo_name}/blob/master/Casks/${cask_name}.rb" + version="${3}" + appcast_url="${4}" + + message="$(echo "Outdated cask: ${cask_name} + + Outdated cask: [\`${cask_name}\`](${cask_url}). + + Info: + + version: \`${version}\`. + + appcast url: ${appcast_url}. + " | sed -E 's/^ {4}//')" + + issue_number=$(ghi open --label 'outdated appcast' --message "${message}" | head -1 | perl -pe 's/^#(\d+): .*/\1/') + message "Opened issue: https://github.com/caskroom/${repo_name}/issues/${issue_number}." +} + +function is_appcast_available { + local appcast_url + + appcast_url="${1}" + + http_status="$(curl "${curl_flags[@]}" --head --write-out '%{http_code}' "${appcast_url}" -o '/dev/null')" + + [[ "${http_status}" == 200 ]] +} + +function report_outdated_appcasts { + local repo_name cask_name appcast_url current_checkpoint new_checkpoint version + + repo_name="${1}" + + for cask_file in ./*; do + appcast_url="$(brew cask _stanza appcast "${cask_file}")" + [[ -z "${appcast_url}" ]] && continue # skip early if there is no appcast + + cask_name="$(basename "${cask_file%.*}")" + + message "Verifying appcast checkpoint for ${cask_name}…" + + if is_appcast_available "${appcast_url}"; then + current_checkpoint="$(brew cask _stanza --yaml appcast "${cask_file}" | grep '^- :checkpoint' | awk '{print $3}')" + new_checkpoint="$(curl "${curl_flags[@]}" --compressed "${appcast_url}" | sed 's|[^<]*||g' | shasum --algorithm 256 | awk '{ print $1 }')" + else + message "There was an error checking the appcast for ${cask_name}." + inaccessible_appcasts+=("${repo_name}/${cask_name}") + continue + fi + + if [[ "${current_checkpoint}" != "${new_checkpoint}" ]]; then + version="$(brew cask _stanza version "${cask_file}")" + + message "${cask_name} is outdated. Opening issue in ${repo_name}…" + open_issue "${repo_name}" "${cask_name}" "${version}" "${appcast_url}" + fi + done +} + +for repo in "${caskroom_repos[@]}"; do + go_to_repo_and_update "${repo}" + report_outdated_appcasts "${repo}" +done + +if [[ ${#inaccessible_appcasts[@]} -gt 0 ]];then + echo # empty line + message 'Some casks have appcasts that errored out, and may need to be rechecked:' + printf '%s\n' "${inaccessible_appcasts[@]}" +fi diff --git a/Library/Homebrew/cask/developer/bin/find_sparkle_appcast b/Library/Homebrew/cask/developer/bin/find_sparkle_appcast new file mode 100755 index 0000000000..b3b854f39e --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/find_sparkle_appcast @@ -0,0 +1,68 @@ +#!/bin/bash + +readonly user_agent=(--user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36') + +usage() { + local program exit_status + + program="$(basename "$0")" + exit_status="$1" + + echo "usage: ${program} " + exit "${exit_status}" +} + +absolute_path() { + echo "$(cd "$(dirname "$1")" && pwd)/$(basename "$1")" +} + +appcast_found_error() { + local error_reason="$1" + + echo "An appcast was found pointing to ${appcast_url}, but it ${error_reason}. You should: + + 1. Check your internet connection. + 2. Try again later. + 3. Contact the developer." + + exit 1 +} + +# exit if no argument (or more than one) was given +if [[ -z "$1" ]] || [[ -n "$2" ]]; then + usage 1 +fi + +# get plist +path_to_app="$(absolute_path "$1")" +path_to_plist="${path_to_app}/Contents/Info.plist" + +if [[ ! -f "${path_to_plist}" ]]; then + echo 'You need to use this on a .app bundle. Please verify your target.' + usage 1 +fi + +# get appcast +appcast_url="$(defaults read "${path_to_plist}" 'SUFeedURL' 2>/dev/null)" + +if [[ -z "${appcast_url}" ]]; then + echo 'It appears this app does not have a Sparkle appcast' + exit 0 +fi + +# validate appcast +appcast_http_response="$(curl --silent --head "${user_agent[@]}" --write-out '%{http_code}' "${appcast_url}" -o /dev/null)" +[[ "${appcast_http_response}" != '200' ]] && appcast_found_error "returned a non-200 (OK) HTTP response code (${appcast_http_response})" + +appcast_checkpoint=$(curl --silent --compressed --location "${user_agent[@]}" "${appcast_url}" | sed 's|[^<]*||g' | shasum --algorithm 256 | awk '{ print $1 }') +[[ "${appcast_checkpoint}" == 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' ]] && appcast_found_error 'seems to be empty' + +# output appcast +echo "A Sparkle appcast was found. You should add it to your cask as + + appcast '${appcast_url}', + checkpoint: '${appcast_checkpoint}' + +You should likely also add 'auto_updates true'" + +exit 0 diff --git a/Library/Homebrew/cask/developer/bin/fix_outdated_appcasts b/Library/Homebrew/cask/developer/bin/fix_outdated_appcasts new file mode 100755 index 0000000000..9915b5339b --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/fix_outdated_appcasts @@ -0,0 +1,78 @@ +#!/bin/bash + +IFS=$'\n' + +readonly caskroom_repos_dir='/tmp/caskroom_repos' +readonly caskroom_repos=(homebrew-cask homebrew-versions homebrew-fonts homebrew-eid) + +if [[ ! $(which 'ghi') ]] || ! security find-internet-password -s github.com -l 'ghi token' &> /dev/null; then + echo -e "$(tput setaf 1) + This script requires 'ghi' installed and configured. + If you have [Homebrew](http://brew.sh), you can install it with 'brew install ghi'. + To configure it, run 'ghi config --auth '. Your Github password will be required, but is never stored. + $(tput sgr0)" | sed -E 's/ {4}//' >&2 + exit 1 +fi + +if [[ ! $(which 'cask-repair') ]]; then + echo -e "$(tput setaf 1) + This script requires 'cask-repair'. + If you have [Homebrew](http://brew.sh), you can install it with 'brew install vitorgalvao/tiny-scripts/cask-repair'. + $(tput sgr0)" | sed -E 's/ {4}//' >&2 + exit 1 +fi + +function message { + echo "${1}" +} + +function go_to_repos_dir { + [[ ! -d "${caskroom_repos_dir}" ]] && mkdir -p "${caskroom_repos_dir}" + cd "${caskroom_repos_dir}" || exit 1 +} + +function go_to_repo_and_update { + local repo_name repo_dir casks_dir + + repo_name="${1}" + repo_dir="${caskroom_repos_dir}/${repo_name}" + casks_dir="${repo_dir}/Casks" + + if [[ ! -d "${repo_dir}" ]]; then + go_to_repos_dir + + message "Cloning ${repo_name}…" + git clone "https://github.com/caskroom/${repo_name}.git" --quiet + + cd "${casks_dir}" || exit 1 + else + cd "${casks_dir}" || exit 1 + + message "Updating ${repo_name}…" + git pull --rebase origin master --quiet + fi +} + +function fix_outdated_appcasts { + local issue_number cask_name pr_number + + for line in $(ghi list --state open --no-pulls --label 'outdated appcast' --reverse | tail +2); do + [[ "${line}" == 'None.' ]] && break # exit early if there are no relevant issues in repo + + issue_number="$(awk '{print $1}' <<< "${line}")" + cask_name="$(awk '{print $4}' <<< "${line}")" + + cask-repair --pull origin --push origin --open-appcast --closes-issue "${issue_number}" --blind-submit "${cask_name}" + + if [[ "$?" -eq 0 ]]; then + pr_number="$(ghi list --pulls --creator | sed -n 2p | awk '{print $1}')" + ghi edit --label 'outdated appcast' "${pr_number}" &>/dev/null + ghi comment --close --message "Closing in favour of #${pr_number}." "${issue_number}" &>/dev/null + fi + done +} + +for repo in "${caskroom_repos[@]}"; do + go_to_repo_and_update "${repo}" + fix_outdated_appcasts +done diff --git a/Library/Homebrew/cask/developer/bin/generate_cask_token b/Library/Homebrew/cask/developer/bin/generate_cask_token new file mode 100755 index 0000000000..48e9331361 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/generate_cask_token @@ -0,0 +1,418 @@ +#!/System/Library/Frameworks/Ruby.framework/Versions/2.0/usr/bin/ruby +# +# generate_cask_token +# +# todo: +# +# remove Ruby 2.0 dependency and change shebang line +# +# detect Cask files which differ only by the placement of hyphens. +# +# merge entirely into "brew cask create" command +# + +### +### dependencies +### + +require "pathname" +require "open3" + +begin + # not available by default + require "active_support/inflector" +rescue LoadError +end + +### +### configurable constants +### + +EXPANDED_SYMBOLS = { + "+" => "plus", + "@" => "at", + }.freeze + +CASK_FILE_EXTENSION = ".rb".freeze + +# Hardcode App names that cannot be transformed automatically. +# Example: in "x48.app", "x48" is not a version number. +# The value in the hash should be a valid Cask token. +APP_EXCEPTION_PATS = { + # looks like a trailing version, but is not. + %r{\Aiterm\Z}i => "iterm2", + %r{\Aiterm2\Z}i => "iterm2", + %r{\Apgadmin3\Z}i => "pgadmin3", + %r{\Ax48\Z}i => "x48", + %r{\Avitamin-r[\s\d\.]*\Z}i => "vitamin-r", + %r{\Aimagealpha\Z}i => "imagealpha", + # upstream is in the midst of changing branding + %r{\Abitcoin-?qt\Z}i => "bitcoin-core", + # "mac" cannot be separated from the name because it is in an English phrase + %r{\Aplayonmac\Z}i => "playonmac", + %r{\Acleanmymac[\s\d\.]*\Z}i => "cleanmymac", + # arguably we should not have kept these two exceptions + %r{\Akismac\Z}i => "kismac", + %r{\Avoicemac\Z}i => "voicemac", + }.freeze + +# Preserve trailing patterns on App names that could be mistaken +# for version numbers, etc +PRESERVE_TRAILING_PATS = [ + %r{id3}i, + %r{mp3}i, + %r{3[\s-]*d}i, + %r{diff3}i, + %r{\A[^\d]+\+\Z}i, + ].freeze + +# The code that employs these patterns against App names +# - hacks a \b (word-break) between CamelCase and snake_case transitions +# - anchors the pattern to end-of-string +# - applies the patterns repeatedly until there is no match +REMOVE_TRAILING_PATS = [ + # spaces + %r{\s+}i, + + # generic terms + %r{\bapp}i, + %r{\b(?:quick[\s-]*)?launcher}i, + + # "mac", "for mac", "for OS X", "macOS", "for macOS". + %r{\b(?:for)?[\s-]*mac(?:intosh|OS)?}i, + %r{\b(?:for)?[\s-]*os[\s-]*x}i, + + # hardware designations such as "for x86", "32-bit", "ppc" + %r{(?:\bfor\s*)?x.?86}i, + %r{(?:\bfor\s*)?\bppc}i, + %r{(?:\bfor\s*)?\d+.?bits?}i, + + # frameworks + %r{\b(?:for)?[\s-]*(?:oracle|apple|sun)*[\s-]*(?:jvm|java|jre)}i, + %r{\bgtk}i, + %r{\bqt}i, + %r{\bwx}i, + %r{\bcocoa}i, + + # localizations + %r{en\s*-\s*us}i, + + # version numbers + %r{[^a-z0-9]+}i, + %r{\b(?:version|alpha|beta|gamma|release|release.?candidate)(?:[\s\.\d-]*\d[\s\.\d-]*)?}i, + %r{\b(?:v|ver|vsn|r|rc)[\s\.\d-]*\d[\s\.\d-]*}i, + %r{\d+(?:[a-z\.]\d+)*}i, + %r{\b\d+\s*[a-z]}i, + %r{\d+\s*[a-c]}i, # constrained to a-c b/c of false positives + ].freeze + +# Patterns which are permitted (undisturbed) following an interior version number +AFTER_INTERIOR_VERSION_PATS = [ + %r{ce}i, + %r{pro}i, + %r{professional}i, + %r{client}i, + %r{server}i, + %r{host}i, + %r{viewer}i, + %r{launcher}i, + %r{installer}i, + ].freeze + +### +### classes +### + +class AppName < String + def self.remove_trailing_pat + @@remove_trailing_pat ||= %r{(?<=.)(?:#{REMOVE_TRAILING_PATS.join('|')})\Z}i + end + + def self.preserve_trailing_pat + @@preserve_trailing_pat ||= %r{(?:#{PRESERVE_TRAILING_PATS.join('|')})\Z}i + end + + def self.after_interior_version_pat + @@after_interior_version_pat ||= %r{(?:#{AFTER_INTERIOR_VERSION_PATS.join('|')})}i + end + + def english_from_app_bundle + return self if ascii_only? + return self unless File.exist?(self) + + # check Info.plist CFBundleDisplayName + bundle_name = Open3.popen3(*%w[ + /usr/libexec/PlistBuddy -c + ], + "Print CFBundleDisplayName", + Pathname.new(self).join("Contents", "Info.plist").to_s) do |_stdin, stdout, _stderr| + begin + stdout.gets.force_encoding("UTF-8").chomp + rescue + end + end + return AppName.new(bundle_name) if bundle_name && bundle_name.ascii_only? + + # check Info.plist CFBundleName + bundle_name = Open3.popen3(*%w[ + /usr/libexec/PlistBuddy -c + ], + "Print CFBundleName", + Pathname.new(self).join("Contents", "Info.plist").to_s) do |_stdin, stdout, _stderr| + begin + stdout.gets.force_encoding("UTF-8").chomp + rescue + end + end + return AppName.new(bundle_name) if bundle_name && bundle_name.ascii_only? + + # check localization strings + local_strings_file = Pathname.new(self).join("Contents", "Resources", "en.lproj", "InfoPlist.strings") + local_strings_file = Pathname.new(self).join("Contents", "Resources", "English.lproj", "InfoPlist.strings") unless local_strings_file.exist? + if local_strings_file.exist? + bundle_name = File.open(local_strings_file, "r:UTF-16LE:UTF-8") do |fh| + %r{\ACFBundle(?:Display)?Name\s*=\s*"(.*)";\Z}.match(fh.readlines.grep(%r{^CFBundle(?:Display)?Name\s*=\s*}).first) do |match| + match.captures.first + end + end + return AppName.new(bundle_name) if bundle_name && bundle_name.ascii_only? + end + + # check Info.plist CFBundleExecutable + bundle_name = Open3.popen3(*%w[ + /usr/libexec/PlistBuddy -c + ], + "Print CFBundleExecutable", + Pathname.new(self).join("Contents", "Info.plist").to_s) do |_stdin, stdout, _stderr| + begin + stdout.gets.force_encoding("UTF-8").chomp + rescue + end + end + return AppName.new(bundle_name) if bundle_name && bundle_name.ascii_only? + + self + end + + def basename + if Pathname.new(self).exist? + AppName.new(Pathname.new(self).basename.to_s) + else + self + end + end + + def remove_extension + sub(%r{\.app\Z}i, "") + end + + def decompose_to_ascii + # crudely (and incorrectly) decompose extended latin characters to ASCII + return self if ascii_only? + return self unless respond_to?(:mb_chars) + AppName.new(mb_chars.normalize(:kd).each_char.select(&:ascii_only?).join) + end + + def hardcoded_exception + APP_EXCEPTION_PATS.each do |regexp, exception| + return AppName.new(exception) if regexp.match(self) + end + nil + end + + def insert_vertical_tabs_for_camel_case + app_name = AppName.new(self) + if app_name.sub!(%r{(#{self.class.preserve_trailing_pat})\Z}i, "") + trailing = Regexp.last_match(1) + end + app_name.gsub!(%r{([^A-Z])([A-Z])}, "\\1\v\\2") + app_name.sub!(%r{\Z}, trailing) if trailing + app_name + end + + def insert_vertical_tabs_for_snake_case + gsub(%r{_}, "\v") + end + + def clean_up_vertical_tabs + gsub(%r{\v}, "") + end + + def remove_interior_versions! + # done separately from REMOVE_TRAILING_PATS because this + # requires a substitution with a backreference + sub!(%r{(?<=.)[\.\d]+(#{self.class.after_interior_version_pat})\Z}i, '\1') + sub!(%r{(?<=.)[\s\.\d-]*\d[\s\.\d-]*(#{self.class.after_interior_version_pat})\Z}i, '-\1') + end + + def remove_trailing_strings_and_versions + app_name = insert_vertical_tabs_for_camel_case + .insert_vertical_tabs_for_snake_case + while self.class.remove_trailing_pat.match(app_name) && + !self.class.preserve_trailing_pat.match(app_name) + app_name.sub!(self.class.remove_trailing_pat, "") + end + app_name.remove_interior_versions! + app_name.clean_up_vertical_tabs + end + + def simplified + return @simplified if @simplified + @simplified = english_from_app_bundle + .basename + .decompose_to_ascii + .remove_extension + @simplified = @simplified.hardcoded_exception || @simplified.remove_trailing_strings_and_versions + @simplified + end +end + +class CaskFileName < String + def spaces_to_hyphens + gsub(%r{ +}, "-") + end + + def delete_invalid_chars + gsub(%r{[^a-z0-9-]+}, "") + end + + def collapse_multiple_hyphens + gsub(%r{--+}, "-") + end + + def delete_leading_hyphens + gsub(%r{^--+}, "") + end + + def delete_hyphens_before_numbers + gsub(%r{-([0-9])}, '\1') + end + + def spell_out_symbols + cask_file_name = self + EXPANDED_SYMBOLS.each do |k, v| + cask_file_name.gsub!(k, " #{v} ") + end + cask_file_name.sub(%r{ +\Z}, "") + end + + def add_extension + sub(%r{(?:#{escaped_cask_file_extension})?\Z}i, CASK_FILE_EXTENSION) + end + + def remove_extension + sub(%r{#{escaped_cask_file_extension}\Z}i, "") + end + + def from_simplified_app_name + return @from_simplified_app_name if @from_simplified_app_name + @from_simplified_app_name = if APP_EXCEPTION_PATS.rassoc(remove_extension) + remove_extension + else + remove_extension + .downcase + .spell_out_symbols + .spaces_to_hyphens + .delete_invalid_chars + .collapse_multiple_hyphens + .delete_leading_hyphens + .delete_hyphens_before_numbers + end + raise "Could not determine Simplified App name" if @from_simplified_app_name.empty? + @from_simplified_app_name.add_extension + end +end + +### +### methods +### + +def project_root + Dir.chdir File.dirname(File.expand_path(__FILE__)) + @git_root ||= Open3.popen3(*%w[ + git rev-parse --show-toplevel + ]) do |_stdin, stdout, _stderr| + begin + Pathname.new(stdout.gets.chomp) + rescue + raise "could not find project root" + end + end + raise "could not find project root" unless @git_root.exist? + @git_root +end + +def escaped_cask_file_extension + @escaped_cask_file_extension ||= Regexp.escape(CASK_FILE_EXTENSION) +end + +def simplified_app_name + @simplified_app_name ||= AppName.new(ARGV.first.dup.force_encoding("UTF-8")).simplified +end + +def cask_file_name + @cask_file_name ||= CaskFileName.new(simplified_app_name).from_simplified_app_name +end + +def cask_token + @cask_token ||= cask_file_name.remove_extension +end + +def warnings + return @warnings if @warnings + @warnings = [] + unless APP_EXCEPTION_PATS.rassoc(cask_token) + if %r{\d} =~ cask_token + @warnings.push "WARNING: '#{cask_token}' contains digits. Digits which are version numbers should be removed." + end + end + filename = project_root.join("Casks", cask_file_name) + if filename.exist? + @warnings.push "WARNING: the file '#{filename}' already exists. Prepend the vendor name if this is not a duplicate." + end + @warnings +end + +def report + puts "Proposed Simplified App name: #{simplified_app_name}" if $debug + puts "Proposed token: #{cask_token}" + puts "Proposed file name: #{cask_file_name}" + puts "Cask Header Line: cask '#{cask_token}' do" + unless warnings.empty? + $stderr.puts "\n" + $stderr.puts warnings + $stderr.puts "\n" + exit 1 + end +end + +### +### main +### + +usage = <<-EOS +Usage: generate_cask_token [ -debug ] + +Given an Application name or a path to an Application, propose a +Cask token, filename, and header line. + +With -debug, also provide the internal "Simplified App Name". + +EOS + +if ARGV.first =~ %r{^-+h(elp)?$}i + puts usage + exit 0 +end + +if ARGV.first =~ %r{^-+debug?$}i + $debug = 1 + ARGV.shift +end + +unless ARGV.length == 1 + puts usage + exit 1 +end + +report diff --git a/Library/Homebrew/cask/developer/bin/generate_issue_template_urls b/Library/Homebrew/cask/developer/bin/generate_issue_template_urls new file mode 100755 index 0000000000..916d849875 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/generate_issue_template_urls @@ -0,0 +1,74 @@ +#!/usr/bin/env ruby +# +# generate_issue_template_urls +# + +### +### dependencies +### + +require "erb" + +### +### constants +### + +BASE_URL = "https://github.com/caskroom/homebrew-cask/issues/new".freeze + +### +### methods +### + +def main(args) + args.each do |file| + File.read(file).scan(%r{(.*?)\n(.*)}m) do |title, body| + puts generate_url(title, body) + end + end +end + +def generate_url(title, body) + encoded_title = url_encode(title) + encoded_body = url_encode(body) + if $debug + puts "Encoded title: #{encoded_title}" + puts "Encoded body: #{encoded_body}" + end + "#{BASE_URL}?title=#{encoded_title}&body=#{encoded_body}" +end + +def url_encode(unencoded_str) + ERB::Util.url_encode(unencoded_str) +end + +### +### main +### + +usage = <<-EOS +Usage: generate_issue_template_urls ... + +Given one or more GitHub issue template files, generate encoded URLs for each +and print, separated by newlines. The first line of a template file should be +the issue title. + +With -debug, print out the encoded title and body individually as well. + +EOS + +if ARGV.first =~ %r{^-+h(elp)?$}i + puts usage + exit 0 +end + +if ARGV.first =~ %r{^-+debug?$}i + $debug = 1 + ARGV.shift +end + +if ARGV.empty? + puts usage + exit 1 +end + +main(ARGV) diff --git a/Library/Homebrew/cask/developer/bin/irregular_cask_whitespace b/Library/Homebrew/cask/developer/bin/irregular_cask_whitespace new file mode 100755 index 0000000000..62a60ae737 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/irregular_cask_whitespace @@ -0,0 +1,133 @@ +#!/bin/bash +# +# irregular_cask_whitespace +# +# find irregular whitespace in Cask files +# +# notes +# +# requires a recent-ish Perl with Unicode support, probably 5.14 +# or better. +# +# bugs +# +# todo +# + +### +### settings +### + +set -e +set -o pipefail +set +o histexpand +set -o nounset +shopt -s nocasematch +shopt -s nullglob + +### +### functions +### + +warn () { + local message="$@" + message="${message//\\t/$'\011'}" + message="${message//\\n/$'\012'}" + message="${message%"${message##*[![:space:]]}"}" + printf "%s\n" "$message" 1>&2 +} + +die () { + warn "$@" + exit 1 +} + +### +### main +### + +_irregular_cask_whitespace () { + local directory="$1" + cd "$directory" || die "Could not cd to '$directory'" + + printf "# No trailing newline at EOF\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{[^\n]\z}s' -- ./*.rb + + printf "\n# Extra trailing newline at EOF\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{\n{2}\z}s' -- ./*.rb + + printf "\n# Final 'end' indented\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{ end\s+\z}s' -- ./*.rb + + printf "\n# Extra newline before final end\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{\n\nend\s+\z}s' -- ./*.rb + + printf "\n# Extra newline before header\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{\n\ncask\s+:v\d\S*\s+=>}s' -- ./*.rb + + printf "\n# Extra newline after header\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{(?:\A|\n)cask\s+:v\d\S*\s+=>[^\n]+\n\n\s*(\S+)}s and $1 ne "if"' -- ./*.rb + + printf "\n# No empty line before uninstall\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{\n[^\n]+\n +uninstall }s' -- ./*.rb + + # todo? + # printf "\n# No empty line before caveats\n" + # perl -C32 -0777 -ne 'print " $ARGV\n" if m{\n[^\n]+\n +caveats }s' -- ./*.rb + + printf "\n# Extra interior newlines\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{\n{3,}}s' -- ./*.rb + + printf "\n# Leading whitespace at BOF\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{\A\s}s' -- ./*.rb + + printf "\n# Trailing whitespace at EOL (includes Tab/CR)\n" + perl -C32 -ne 'print " $ARGV\n" if m{\s\n}s' -- ./*.rb | sort | uniq + + printf "\n# Tabs\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{\t}s' -- ./*.rb + + printf "\n# Carriage Returns\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{\r}s' -- ./*.rb + + printf "\n# Misc Control Characters\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{[\x00-\x08\x0B-\x0C\x0E\x1F]}s' -- ./*.rb + + printf "\n# First indent not 2\n" + perl -C32 -0777 -ne 's{\A(.*?\n)?cask\s+[^\n]+\n+}{}s; print " $ARGV\n" unless m{\A \S}s' -- ./*.rb + + printf "\n# Indents not multiple of 2\n" + perl -C32 -0777 -ne 'print " $ARGV\n" if m{\n(?: ){0,} [a-z]}s' -- ./*.rb + + printf "\n# Unicode Space Characters\n" + # \x{0085}\x{0088}\x{0089} + perl -C32 -0777 -ne 'print " $ARGV\n" if m{[\x{008a}\x{00a0}\x{1680}\x{180e}\x{2000}\x{2001}\x{2002}\x{2003}\x{2004}\x{2005}\x{2006}\x{2007}\x{2008}\x{2009}\x{200a}\x{200b}\x{2028}\x{2029}\x{202f}\x{205f}\x{2060}\x{3000}\x{feff}\x{e0020}]}s' -- ./*.rb + +} + +### +### argument processing +### + +if [[ "${1:-}" =~ ^-+h(elp)?$ ]]; then + printf "irregular_cask_whitespace + + Find irregular whitespace in Cask files within + +" + exit +fi + +if [ "$#" -ne 1 ]; then + die "Single directory argument required" +elif ! [ -d "$1" ]; then + die "No directory found at '$1'" +fi + +### +### dispatch +### + +_irregular_cask_whitespace "${@:-}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_apps_in_pkg b/Library/Homebrew/cask/developer/bin/list_apps_in_pkg new file mode 100755 index 0000000000..9e0411322f --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_apps_in_pkg @@ -0,0 +1,166 @@ +#!/bin/bash +# +# list_apps_in_pkg +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### global variables +### + +opt_lax='' +opt_pkg='' +pkgdir='' + +# prefer GNU xargs +xargs="$(/usr/bin/which gxargs || printf '/usr/bin/xargs')" + +### +### functions +### + +warn () { + local message="$@" + message="${message//\\t/$'\011'}" + message="${message//\\n/$'\012'}" + message="${message%"${message##*[![:space:]]}"}" + printf "%s\n" "$message" 1>&2 +} + +die () { + warn "$@" + exit 1 +} + +app_source_1 () { + /usr/bin/find "$pkgdir" -name PackageInfo -print0 | \ + "$xargs" -0 /usr/bin/perl -0777 -ne \ + 'while (m{ + +Given a package file, extract a list of candidate App names from +inside the pkg, which may be useful for naming a Cask. + +The given package file need not be installed. + +If an App of the listed name is already installed in /Applications +or ~/Applications, it will be followed by a plus symbol '(+)' in +the output. This can be verified via 'ls' or the Finder. + +Arguments + + -lax Be less selective in looking for App names. Generate + more, but less accurate, guesses. + +Bugs: This script is imperfect. + - It does not fully parse PackageInfo files + - An App can be hidden within a nested archive and not found + - Some pkg files simply don't contain any Apps + +See CONTRIBUTING.md and 'man pkgutil' for more information. + +" + exit + elif [[ $arg =~ ^-+lax$ ]]; then + opt_lax='true' + elif [[ "$arg" = "$opt_pkg" ]]; then + true + else + die "ERROR: Unknown argument '$arg'" + fi + done + if [[ -h "$opt_pkg" ]]; then + opt_pkg="$(/usr/bin/readlink "$opt_pkg")" + fi + if ! [[ -e "$opt_pkg" ]]; then + die "ERROR: No such pkg file: '$opt_pkg'" + fi +} + +### +### main +### + +_list_apps_in_pkg () { + + if [[ -d "$opt_pkg" ]]; then + pkgdir="$opt_pkg" + else + local tmpdir="$(/usr/bin/mktemp -d -t list_ids_in_pkg)" + trap "/bin/rm -rf -- '$tmpdir'" EXIT + pkgdir="$tmpdir/unpack" + /usr/sbin/pkgutil --expand "$opt_pkg" "$tmpdir/unpack" "$pkgdir" + fi + + { + # strings that look like App names (Something.app) + app_source_1; + app_source_2; + app_source_3; + if [[ -n "$opt_lax" ]]; then + app_source_4; + app_source_5; + fi + } | \ + merge_sources | \ + mark_up_sources +} + +process_args "${@}" + +# dispatch main +_list_apps_in_pkg + +# diff --git a/Library/Homebrew/cask/developer/bin/list_id_in_kext b/Library/Homebrew/cask/developer/bin/list_id_in_kext new file mode 100755 index 0000000000..b1ee95a771 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_id_in_kext @@ -0,0 +1,93 @@ +#!/bin/bash +# +# list_id_in_kext +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### global variables +### + +kextdir='' + +# prefer GNU xargs +xargs="$(/usr/bin/which gxargs || printf '/usr/bin/xargs')" + +### +### functions +### + +bundle_id_source_1 () { + /usr/bin/find "$kextdir" -name Info.plist -print0 | \ + "$xargs" -0 -I {} /usr/libexec/PlistBuddy {} -c 'Print :CFBundleIdentifier' +} + +merge_sources () { + /usr/bin/sort | /usr/bin/uniq +} + +clean_sources () { + /usr/bin/egrep -v '^com\.apple\.' +} + +mark_up_sources () { + /usr/bin/perl -pe 's{\n}{\000}sg' | \ + "$xargs" -0 -I{} -n1 /bin/bash -c \ + 'printf "{}"; /bin/test "$(/usr/sbin/kextstat -kl -b "{}" | /usr/bin/wc -l)" -gt 0 && printf " (+)"; printf "\n"' +} + +### +### main +### + +_list_id_in_kext () { + + kextdir="$1" + if [[ -h "$kextdir" ]]; then + kextdir="$(/usr/bin/readlink "$kextdir")" + fi + + { + # emit strings that look like bundle ids + bundle_id_source_1; + } | \ + clean_sources | \ + merge_sources | \ + mark_up_sources + +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ || -z "$1" ]]; then + printf "list_id_in_kext + +Given a Kernel Extension (kext) bundle dir on disk, extract the +associated kext Bundle ID, which may be useful in a Cask uninstall +stanza, eg + + uninstall :kext => 'kext.id.goes.here' + +The kext need not be loaded for this script to work. + +If a given kext is currently loaded, it will be followed by a plus +symbol '(+)' in the output. This can be verified via the command + + /usr/sbin/kextstat -kl -b 'kext.id.goes.here' + +See CONTRIBUTING.md and 'man kextstat' for more information. + +" + exit +fi + +# dispatch main +_list_id_in_kext "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_ids_in_app b/Library/Homebrew/cask/developer/bin/list_ids_in_app new file mode 100755 index 0000000000..89bb253a5f --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_ids_in_app @@ -0,0 +1,162 @@ +#!/bin/bash +# +# list_ids_in_app +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### global variables +### + +appdir='' +scriptdir="$(dirname "$0")" + +# prefer GNU xargs +xargs="$(/usr/bin/which gxargs || printf '/usr/bin/xargs')" + +### +### functions +### + +bundle_id_source_1 () { + /usr/bin/find "$appdir" -name Info.plist -print0 | \ + "$xargs" -0 -I {} /usr/libexec/PlistBuddy {} -c 'Print :CFBundleIdentifier' +} + +merge_sources () { + /usr/bin/sort | /usr/bin/uniq +} + +clean_sources () { + /usr/bin/egrep -v '^com\.apple\.' | \ + /usr/bin/egrep -vi '^org\.andymatuschak\.Sparkle' | \ + /usr/bin/egrep -vi '^(SDL|SDL_net|TDParseKit)$' | \ + /usr/bin/egrep -vi '^com\.growl\.(growlframework|WebKit|iCal|GrowlAction|GrowlLauncher|GrowlPlugins)' | \ + /usr/bin/egrep -vi '^com\.adobe\.(ACE|AGM|AXE8SharedExpat|AdobeExtendScript|AdobeScCore|AdobeXMPCore|BIB|BIBUtils|CoolType|ESD\.AdobeUpdaterLibFramework|JP2K|adobe_caps|AcrobatPlugin|AdobeResourceSynchronizer|ICUConverter|ICUData|acrobat\.assert|acrobat\.pdfviewerNPAPI|adobepdf417pmp|ahclientframework|datamatrixpmp|eulaframework|framework|linguistic|qrcodepmp)' | \ + /usr/bin/egrep -vi '^com\.microsoft\.(Automator|certificate\.framework|chart|converterlib|converters|excel\.pde|grammar|igx|mcp|merp|msls3|netlib|officeart|ole|oleo|powerplant|powerplantcore|powerpoint\.pde|speller|thesaurus|urlmon|wizard|wordforms|MSCommon|mbuinstrument_framework|mbukernel_framework|rdpkit)' | \ + /usr/bin/egrep -vi '^com\.google\.(Chrome\.framework|Chrome\.helper|Keystone|BreakpadFramework|GDataFramework|Reporter)' | \ + /usr/bin/egrep -vi '^atmo\.mac\.macho' | \ + /usr/bin/egrep -vi '^com\.3dconnexion\.driver\.client' | \ + /usr/bin/egrep -vi '^com\.Breakpad\.crash_report_sender' | \ + /usr/bin/egrep -vi '^com\.Cycling74\.driver\.Soundflower' | \ + /usr/bin/egrep -vi '^com\.HumbleDaisy\.HDCrashReporter' | \ + /usr/bin/egrep -vi '^com\.amazon\.JSONKit' | \ + /usr/bin/egrep -vi '^com\.bensyverson\..*dvmatte' | \ + /usr/bin/egrep -vi '^com\.binarymethod\.BGHUDAppKit' | \ + /usr/bin/egrep -vi '^com\.blacktree\.(QSCore|QSEffects|QSFoundation|QSInterface)' | \ + /usr/bin/egrep -vi '^com\.brandonwalkin\.BWToolkitFramework' | \ + /usr/bin/egrep -vi '^com\.cruzapp\.TDWebThumbnail' | \ + /usr/bin/egrep -vi '^com\.cycling74\.QTExportTool' | \ + /usr/bin/egrep -vi '^com\.fluidapp\.(BrowserBrowserPlugIn|FluidInstance|ThumbnailPlugIn)' | \ + /usr/bin/egrep -vi '^com\.github\.ObjectiveGit' | \ + /usr/bin/egrep -vi '^com\.heroku\.RedisAdapter' | \ + /usr/bin/egrep -vi '^com\.instinctivecode\.MGScopeBar' | \ + /usr/bin/egrep -vi '^com\.intel\.nw\.helper' | \ + /usr/bin/egrep -vi '^com\.joshaber\.RockemSockem' | \ + /usr/bin/egrep -vi '^com\.katidev\.KTUIKit' | \ + /usr/bin/egrep -vi '^com\.kirin\.plugin\.adapter' | \ + /usr/bin/egrep -vi '^com\.lextek\.onix' | \ + /usr/bin/egrep -vi '^com\.macromedia\.(Flash Player\.authplaylib|PepperFlashPlayer)' | \ + /usr/bin/egrep -vi '^com\.mainconcept\.mc\.enc\.avc' | \ + /usr/bin/egrep -vi '^com\.netscape\.(DefaultPlugin|MRJPlugin)' | \ + /usr/bin/egrep -vi '^com\.nxtbgthng\.JSONKit' | \ + /usr/bin/egrep -vi '^com\.omnigroup\.(OmniAppKit|OmniInspector|framework)' | \ + /usr/bin/egrep -vi '^com\.oracle\.java\..*\.jdk' | \ + /usr/bin/egrep -vi '^com\.panic\.(PanicCore|automator|CodaScriptPlugIn)' | \ + /usr/bin/egrep -vi '^com\.pixelespresso\.cocoafob' | \ + /usr/bin/egrep -vi '^com\.positivespinmedia\.(PSMTabBarControlFramework|PSMTabBarFramework)' | \ + /usr/bin/egrep -vi '^com\.softube\.(Amplifier|Cabinet)' | \ + /usr/bin/egrep -vi '^com\.sonic\.(AS_Storage|AuthorScriptHDMV)' | \ + /usr/bin/egrep -vi '^com\.soundcloud\.Share-on-SoundCloud' | \ + /usr/bin/egrep -vi '^com\.stuffit\.(format|sdk|stuffitcore)' | \ + /usr/bin/egrep -vi '^com\.sun\.Scintilla' | \ + /usr/bin/egrep -vi '^com\.yourcompany' | \ + /usr/bin/egrep -vi '^coop\.plausible\.(CrashReporter|PLWeakCompatibility)' | \ + /usr/bin/egrep -vi '^de\.buzzworks\.Quincy' | \ + /usr/bin/egrep -vi '^de\.dstoecker\.xadmaster' | \ + /usr/bin/egrep -vi '^isao\.sonobe\.OgreKit' | \ + /usr/bin/egrep -vi '^jp\.hmdt\.framework\.hmdtblkappkit' | \ + /usr/bin/egrep -vi '^net\.hockeyapp\.sdk\.mac' | \ + /usr/bin/egrep -vi '^net\.java\.openjdk\.jre' | \ + /usr/bin/egrep -vi '^net\.liquidx\.EyeTunes' | \ + /usr/bin/egrep -vi '^net\.sourceforge\.Log4Cocoa' | \ + /usr/bin/egrep -vi '^net\.sourceforge\.munt\.MT32Emu' | \ + /usr/bin/egrep -vi '^net\.sourceforge\.skim-app\.framework' | \ + /usr/bin/egrep -vi '^net\.wafflesoftware\.ShortcutRecorder\.framework' | \ + /usr/bin/egrep -vi '^org\.AFNetworking\.AFNetworking' | \ + /usr/bin/egrep -vi '^org\.boredzo\.(LMX|ISO8601DateFormatter)' | \ + /usr/bin/egrep -vi '^org\.dribin\.dave\.DDHidLib' | \ + /usr/bin/egrep -vi '^org\.linkbackproject\.LinkBack' | \ + /usr/bin/egrep -vi '^org\.mozilla\.(crashreporter|plugincontainer|universalchardet|updater)' | \ + /usr/bin/egrep -vi '^org\.python\.(python|PythonLauncher|buildapplet)' | \ + /usr/bin/egrep -vi '^org\.remotesensing\.libtiff' | \ + /usr/bin/egrep -vi '^org\.vafer\.FeedbackReporter' | \ + /usr/bin/egrep -vi '^org\.xiph\.(ogg|vorbis)' | \ + /usr/bin/egrep -vi '^se\.propellerheads\..*\.library$' + +} + +mark_up_sources () { + /usr/bin/perl -pe 's{\n}{\000}sg' | \ + "$xargs" -0 -I{} -n1 /bin/bash -c \ + "printf '{}'; $scriptdir/list_running_app_ids -t '{}' >/dev/null 2>&1 && printf ' (+)'; printf "\\\\n"" +} + +### +### main +### + +_list_ids_in_app () { + + appdir="$1" + if [[ -h "$appdir" ]]; then + appdir="$(/usr/bin/readlink "$appdir")" + fi + + { + # emit strings that look like bundle ids + bundle_id_source_1; + } | \ + clean_sources | \ + merge_sources | \ + mark_up_sources + +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ || -z "$1" ]]; then + printf "list_ids_in_app + +Given a Application (app) bundle directory on disk, extract the +associated app Bundle ID, which may be useful in a Cask uninstall +stanza, eg + + uninstall quit: 'app.id.goes.here' + +The app need not be running for this script to work. + +Bundle IDs attributed to Apple and common developer frameworks +are excluded from the output. + +If a given app is currently running, it will be followed by a plus +symbol '(+)' in the output. This can be verified via the command + + list_running_app_ids | grep 'app.id.goes.here' + +See CONTRIBUTING.md for more information. + +" + exit +fi + +# dispatch main +_list_ids_in_app "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_ids_in_pkg b/Library/Homebrew/cask/developer/bin/list_ids_in_pkg new file mode 100755 index 0000000000..3b0dec4a68 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_ids_in_pkg @@ -0,0 +1,115 @@ +#!/bin/bash +# +# list_ids_in_pkg +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### global variables +### + +pkgdir='' + +# prefer GNU xargs +xargs="$(/usr/bin/which gxargs || printf '/usr/bin/xargs')" + +### +### functions +### + +bundle_id_source_1 () { + /usr/bin/find "$pkgdir" -name PackageInfo -print0 | \ + "$xargs" -0 /usr/bin/perl -0777 -ne \ + 'while (m{/dev/null 2>&1 && printf " (+)"; printf "\n"' +} + +### +### main +### + +_list_ids_in_pkg () { + + if [[ -d "$1" ]]; then + pkgdir="$1" + if [[ -h "$pkgdir" ]]; then + pkgdir="$(/usr/bin/readlink "$pkgdir")" + fi + else + local tmpdir="$(/usr/bin/mktemp -d -t list_ids_in_pkg)" + trap "/bin/rm -rf -- '$tmpdir'" EXIT + pkgdir="$tmpdir/unpack" + /usr/sbin/pkgutil --expand "$1" "$tmpdir/unpack" "$pkgdir" + fi + + { + # emit strings that look like bundle ids + bundle_id_source_1; + bundle_id_source_2; + } | \ + merge_sources | \ + clean_sources | \ + mark_up_sources + +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ || -z "$1" ]]; then + printf "list_ids_in_pkg + +Given a package file, extract a list of candidate Package IDs +which may be useful in a Cask uninstall stanza, eg + + uninstall pkgutil: 'package.id.goes.here' + +The given package file need not be installed. + +The output of this script should be overly inclusive -- not +every candidate package id in the output will be needed at +uninstall time. + +Package IDs designated by Apple or common development frameworks +will be excluded from the output. + +If a package id is already installed, it will be followed by +a plus symbol '(+)' in the output. This can be verified via +the command + + /usr/sbin/pkgutil --pkg-info 'package.id.goes.here' + +See CONTRIBUTING.md and 'man pkgutil' for more information. + +" + exit +fi + +# dispatch main +_list_ids_in_pkg "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_installed_launchjob_ids b/Library/Homebrew/cask/developer/bin/list_installed_launchjob_ids new file mode 100755 index 0000000000..82672c2c9e --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_installed_launchjob_ids @@ -0,0 +1,90 @@ +#!/bin/bash +# +# list_installed_launchjob_ids +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### global variables +### + +# prefer GNU xargs +xargs="$(/usr/bin/which gxargs || printf '/usr/bin/xargs')" + +### +### functions +### + +launchjob_id_source_1 () { + /usr/bin/find ~/Library/LaunchAgents/ \ + ~/Library/LaunchDaemons/ \ + /Library/LaunchAgents/ \ + /Library/LaunchDaemons/ \ + -type f -print0 2>/dev/null | \ + "$xargs" -0 /usr/bin/perl -0777 -ne \ + 'while (m{\s*Label\s*\s*([^<]+?)}sg) { print "$1\n" }' +} + +merge_sources () { + /usr/bin/sort | /usr/bin/uniq +} + +clean_sources () { + /usr/bin/egrep -v '^com\.apple\.' +} + +mark_up_sources () { + /usr/bin/perl -pe 's{\n}{\000}sg' | \ + "$xargs" -0 -I{} -n1 /bin/bash -c \ + 'printf "{}"; /bin/launchctl list "{}" >/dev/null 2>&1 && printf " (+)"; printf "\n"' +} + +### +### main +### + +_list_installed_launchjob_ids () { + + { + launchjob_id_source_1; + } | \ + merge_sources | \ + clean_sources | \ + mark_up_sources + +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ ]]; then + printf "list_installed_launchjob_ids + +List all installed launchjob IDs, which may be useful +in a Cask uninstall stanza, eg + + uninstall launchctl: 'job.id.goes.here' + +Launchctl jobs attributed to Apple will be ommitted. + +If a launchctl job is currently loaded, and visible to the current +user, it will be followed by a plus symbol '(+)' in the output. +This can be verified via the command + + /bin/launchctl list 'job.id.goes.here' + +See CONTRIBUTING.md and 'man launchctl' for more information. + +" + exit +fi + +# dispatch main +_list_installed_launchjob_ids "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_loaded_kext_ids b/Library/Homebrew/cask/developer/bin/list_loaded_kext_ids new file mode 100755 index 0000000000..19b47cb078 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_loaded_kext_ids @@ -0,0 +1,45 @@ +#!/bin/bash +# +# list_loaded_kext_ids +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### main +### + +_list_loaded_kext_ids () { + /usr/sbin/kextstat -kl | \ + /usr/bin/cut -c53- | \ + /usr/bin/cut -f1 -d' ' | \ + /usr/bin/egrep -v '^com\.apple\.' +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ ]]; then + printf "list_loaded_kext_ids + +Print Bundle IDs for currently loaded Kernel Extensions (kexts) +which may be useful in a Cask uninstall stanza, eg + + uninstall kext: 'kext.bundle.id.goes.here' + +Kexts attributed to Apple are excluded from the output. + +See CONTRIBUTING.md for more information. + +" + exit +fi + +# dispatch main +_list_loaded_kext_ids "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_loaded_launchjob_ids b/Library/Homebrew/cask/developer/bin/list_loaded_launchjob_ids new file mode 100755 index 0000000000..4b1330b705 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_loaded_launchjob_ids @@ -0,0 +1,84 @@ +#!/bin/bash +# +# list_loaded_launchjob_ids +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### global variables +### + +### +### functions +### + +launchjob_id_source_1 () { + if [[ "$EUID" -ne 0 ]]; then + /usr/bin/sudo -p 'Optionally give your sudo password: ' -- /bin/launchctl list | /usr/bin/cut -f3 + fi +} + +launchjob_id_source_2 () { + /bin/launchctl list | /usr/bin/cut -f3 +} + +merge_sources () { + /usr/bin/sort | /usr/bin/uniq +} + +clean_sources () { + /usr/bin/egrep -v '^Label$' | \ + /usr/bin/egrep -v '^com\.apple\.' | \ + /usr/bin/egrep -v '^0x[0-9a-f]+\.anonymous\.' | \ + /usr/bin/egrep -v '^\[0x' | \ + /usr/bin/egrep -v '\.[0-9]+$' +} + +### +### main +### + +_list_loaded_launchjob_ids () { + + { + launchjob_id_source_1; + launchjob_id_source_2; + } | \ + clean_sources | \ + merge_sources + +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ ]]; then + printf "list_loaded_launchjob_ids + +List IDs for currently-loaded launchctl jobs, which may be useful +in a Cask uninstall stanza, eg + + uninstall launchctl: 'job.id.goes.here' + +If this command is not run as the superuser, you will be prompted +for a password to run a subcommand using 'sudo'. The password is +not required, but supplying it may reveal additional job ids. To +skip using the password, press repeatedly. + +Launchctl jobs attributed to Apple are excluded from the output. + +See CONTRIBUTING.md and 'man launchctl' for more information. + +" + exit +fi + +# dispatch main +_list_loaded_launchjob_ids "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_login_items_for_app b/Library/Homebrew/cask/developer/bin/list_login_items_for_app new file mode 100755 index 0000000000..f775347efc --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_login_items_for_app @@ -0,0 +1,64 @@ +#!/usr/bin/env ruby +# +# list_login_items_for_app +# + +### +### dependencies +### + +require "open3" + +### +### methods +### + +def usage + <<-EOS +Usage: list_login_items_for_app + +Given an Application (app) bundle directory on disk, find all +login items associated with that app, which you can use in a +Cask uninstall stanza, eg + + uninstall login_item: 'login item name' + +Note that you will likely need to have opened the app at least +once for any login items to be present. + +See CONTRIBUTING.md for more information. + +EOS +end + +def process_args + if ARGV.first =~ %r{^-+h(?:elp)?$} + puts usage + exit 0 + elsif ARGV.length == 1 + $app_path = ARGV.first + else + puts usage + exit 1 + end +end + +def list_login_items_for_app(app_path) + out, err, status = Open3.capture3( + "/usr/bin/osascript", "-e", + "tell application \"System Events\" to get the name of every login item " \ + "whose path contains \"#{File.basename(app_path)}\"" + ) + if status.exitstatus > 0 + $stderr.puts err + exit status.exitstatus + end + puts out.gsub(", ", "\n") +end + +### +### main +### + +process_args +list_login_items_for_app $app_path diff --git a/Library/Homebrew/cask/developer/bin/list_payload_in_pkg b/Library/Homebrew/cask/developer/bin/list_payload_in_pkg new file mode 100755 index 0000000000..cf71bb2aaa --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_payload_in_pkg @@ -0,0 +1,126 @@ +#!/bin/bash +# +# list_payload_in_pkg +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### global variables +### + +pkg_arg='' +tmp_boms='' + +# prefer GNU xargs +xargs="$(/usr/bin/which gxargs || printf '/usr/bin/xargs')" + +trap cleanup_tmp_boms EXIT + +### +### functions +### + +cleanup_tmp_boms () { + if [[ -n "$tmp_boms" ]]; then + # tmpfile ensures that rmdir -p is not too destructive + local tmpfile="/tmp/list_payload_in_pkg.$$"; + /usr/bin/touch "$tmpfile"; + echo "$tmp_boms" | \ + /usr/bin/perl -pe 's{\n}{\000}sg' | \ + "$xargs" -0 /bin/rm -f --; + { + echo "$tmp_boms" | \ + /usr/bin/perl -pe 's{[^/]+\n}{\000}sg' | \ + "$xargs" -0 /bin/rmdir -p -- || true + } 2>/dev/null + /bin/rm -- "$tmpfile"; + fi +} + +bom_source_1 () { + /usr/bin/find "$pkg_arg" -iname '*.pkg' -print0 | \ + "$xargs" -0 -I{} -n1 /usr/sbin/pkgutil --bom "{}" 2>/dev/null +} + +bom_source_2 () { + /usr/bin/find "$pkg_arg" -name '*.bom' +} + +expand_sources () { + /usr/bin/perl -pe 's{\n}{\000}sg' | \ + "$xargs" -0 lsbom -- +} + +merge_sources () { + /usr/bin/sort | /usr/bin/uniq +} + +clean_sources () { + /usr/bin/cut -f1 | \ + /usr/bin/perl -pe 's{\A\.}{}' | \ + /usr/bin/egrep '.' +} + +mark_up_sources () { + /usr/bin/perl -pe 's{\n}{\000}sg' | \ + "$xargs" -0 -I{} -n1 /bin/bash -c \ + 'printf "{}"; /bin/test -e "{}" >/dev/null 2>&1 && printf " (+)"; printf "\n"' +} + +### +### main +### + +_list_payload_in_pkg () { + + pkg_arg="$1" + if [[ -h "$pkg_arg" ]]; then + pkg_arg="$(/usr/bin/readlink "$pkg_arg")" + fi + + tmp_boms="$(bom_source_1)"; + { + # find BOM files + echo "$tmp_boms"; + bom_source_2; + } | \ + expand_sources | \ + clean_sources | \ + merge_sources | \ + mark_up_sources + +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ || -z "$1" ]]; then + printf "list_payload_in_pkg + +Given a package file, show what files may be installed by that +pkg, which may be useful when writing a Cask uninstall stanza. + +The given package file need not be installed. + +The output attempts to be overly inclusive. However, since +pkg files are allowed to run arbitrary scripts, there can be +no guarantee that the output is exact. + +If a given file is already installed, it will be followed by +a plus symbol '(+)' in the output. + +See CONTRIBUTING.md and 'man pkgutil' for more information. + +" + exit +fi + +# dispatch main +_list_payload_in_pkg "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_pkg_ids_by_regexp b/Library/Homebrew/cask/developer/bin/list_pkg_ids_by_regexp new file mode 100755 index 0000000000..6c1f392c99 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_pkg_ids_by_regexp @@ -0,0 +1,83 @@ +#!/bin/bash +# +# list_pkg_ids_by_regexp +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### functions +### + +warn () { + local message="$@" + message="${message//\\t/$'\011'}" + message="${message//\\n/$'\012'}" + message="${message%"${message##*[![:space:]]}"}" + printf "%s\n" "$message" 1>&2 +} + +die () { + warn "$@" + exit 1 +} + +fail_informatively () { + local message="No match." + if ! [[ "$1" =~ '*' ]]; then + message="$message Suggestion: try '${1}.*'" + fi + die "$message" +} + +analyze_regexp () { + if [[ "$1" =~ ^\^ ]]; then + warn "Note: pkgutil regular expressions are implicitly anchored with '^' at start" + fi + if [[ "$1" =~ \$$ ]]; then + warn "Note: pkgutil regular expressions are implicitly anchored with '$' at end" + fi +} + +### +### main +### + +_list_pkg_ids_by_regexp () { + analyze_regexp "$1" + if ! /usr/sbin/pkgutil --pkgs="$1"; then + fail_informatively "$1" + fi +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ || $# -ne 1 ]]; then + printf "list_pkg_ids_by_regexp + +Print pkg receipt IDs for installed packages matching a regular +expression, which may be useful in a Cask uninstall stanza, eg + + uninstall pkgutil: 'pkg.regexp.goes.here' + +Unlike most other scripts in this directory, package IDs attributed to +Apple are NOT excluded from the output. This is to avoid uninstalling +essential system files due to an exuberant regexp. + +For more information, see + + https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/uninstall.md + +" + exit +fi + +# dispatch main +_list_pkg_ids_by_regexp "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_recent_pkg_ids b/Library/Homebrew/cask/developer/bin/list_recent_pkg_ids new file mode 100755 index 0000000000..0c203ec26c --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_recent_pkg_ids @@ -0,0 +1,46 @@ +#!/bin/bash +# +# list_recent_pkg_ids +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### main +### + +_list_recent_pkg_ids () { + /bin/ls -t /var/db/receipts | \ + /usr/bin/egrep '\.plist$' | \ + /usr/bin/perl -pe 's{\A([^/]+)\.plist\Z}{$1}sg' | \ + /usr/bin/egrep -v '^com\.apple\.' | \ + /usr/bin/head -10 +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ ]]; then + printf "list_recent_pkg_ids + +Print pkg receipt IDs for the 10 most-recently-installed packages, +which may be useful in a Cask uninstall stanza, eg + + uninstall pkgutil: 'pkg.receipt.id.goes.here' + +Package IDs attributed to Apple are excluded from the output. + +See CONTRIBUTING.md for more information. + +" + exit +fi + +# dispatch main +_list_recent_pkg_ids "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/list_running_app_ids b/Library/Homebrew/cask/developer/bin/list_running_app_ids new file mode 100755 index 0000000000..136a578d40 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_running_app_ids @@ -0,0 +1,115 @@ +#!/usr/bin/env ruby +# +# list_running_app_ids +# + +### +### dependencies +### + +require "open3" +require "set" + +### +### globals +### + +$opt_test = nil + +### +### methods +### + +def check_ruby + if RUBY_VERSION.to_f < 2.0 + print "You are currently using Ruby ", RUBY_VERSION, ", but version 2.0 or above is required." + exit 1 + end +end + +def usage + <<-EOS +list_running_app_ids [ -t ] + +Print a list of currently running Applications and associated +Bundle IDs, which may be useful in a Cask uninstall stanza, eg + + uninstall quit: 'bundle.id.goes.here' + +Applications attributed to Apple are excluded from the output. + +With optional "-t ", silently test if a given app +is running, exiting with an error code if not. + +See CONTRIBUTING.md for more information. + +EOS +end + +def process_args + until ARGV.empty? + if ARGV.first =~ %r{^-+t(?:est)?$} && ARGV.length > 1 + ARGV.shift + $opt_test = ARGV.shift + elsif ARGV.first =~ %r{^-+h(?:elp)?$} + puts usage + exit 0 + else + puts usage + exit 1 + end + end +end + +def load_apps + out, err, status = Open3.capture3("/usr/bin/osascript", "-e", 'tell application "System Events" to get (name, bundle identifier, unix id) of every process') + if status.exitstatus > 0 + puts err + exit status.exitstatus + end + out = out.split(", ") + one_third = out.length / 3 + @app_names = out.shift(one_third) + @bundle_ids = out.shift(one_third) + @unix_ids = out.shift(one_third) +end + +def test_app(bundle) + @bundle_ids.include?(bundle) ? 0 : 1 +end + +def excluded_bundle_id(bundle_id) + %r{^com\.apple\.}.match(bundle_id) +end + +def excluded_app_name(app_name) + %r{^osascript$}.match(app_name) # this script itself +end + +def report_apps + running = Set.new + @app_names.zip(@bundle_ids, @unix_ids).each do |app_name, bundle_id, _unix_id| + next if excluded_bundle_id bundle_id + next if excluded_app_name app_name + bundle_id.gsub!(%r{^(missing value)$}, '<\1>') + running.add "#{bundle_id}\t#{app_name}" + end + + puts "bundle_id\tapp_name\n" + puts "--------------------------------------\n" + puts running.to_a.sort +end + +### +### main +### + +check_ruby +process_args +load_apps + +if $opt_test + exit test_app($opt_test) +else + report_apps +end diff --git a/Library/Homebrew/cask/developer/bin/list_url_attributes_on_file b/Library/Homebrew/cask/developer/bin/list_url_attributes_on_file new file mode 100755 index 0000000000..e594976560 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/list_url_attributes_on_file @@ -0,0 +1,84 @@ +#!/bin/bash +# +# list_url_attributes_on_file +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### global variables +### + +file_arg='' +attribute_1='com.apple.metadata:kMDItemWhereFroms' + +### +### functions +### + +warn () { + local message="$@" + message="${message//\\t/$'\011'}" + message="${message//\\n/$'\012'}" + message="${message%"${message##*[![:space:]]}"}" + printf "%s\n" "$message" 1>&2 +} + +die () { + warn "$@" + exit 1 +} + +xml_source_1 () { + if /usr/bin/xattr -p "$attribute_1" "$file_arg" > /dev/null 2>&1; then + /usr/bin/xattr -p "$attribute_1" "$file_arg" | /usr/bin/xxd -r -p | /usr/bin/plutil -convert xml1 -o - - 2> /dev/null + fi +} + +extract_string_elements () { + /usr/bin/perl -ne 'print "$1\n" if m{\A\s*<\s*string\s*>(.+?)<\s*/\s*string\s*>\Z}' +} + +### +### main +### + +_list_url_attributes_on_file () { + file_arg="$1" + { + xml_source_1; + } | \ + extract_string_elements +} + +# process_args +if [[ $1 =~ ^-+h(elp)?$ || -z "$1" ]]; then + printf "list_url_attributes_on_file + +Given a downloaded file, extract possible sources from macOS extended +attributes, which may be useful in a Cask url stanza. + +Currently the only attribute examined is + + com.apple.metadata:kMDItemWhereFroms + +This attribute will typically be set if the file was downloaded via a +browser, but not if the file was downloaded by a CLI utility such as +curl. + +See CONTRIBUTING.md for more information. + +" + exit +fi + +# dispatch main +_list_url_attributes_on_file "${@}" + +# diff --git a/Library/Homebrew/cask/developer/bin/merge_outdated_appcasts b/Library/Homebrew/cask/developer/bin/merge_outdated_appcasts new file mode 100755 index 0000000000..0c2b55a89a --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/merge_outdated_appcasts @@ -0,0 +1,99 @@ +#!/bin/bash + +IFS=$'\n' + +readonly caskroom_online='https://github.com/caskroom' +readonly caskroom_repos_dir='/tmp/caskroom_repos' +readonly caskroom_repos=(homebrew-cask homebrew-versions homebrew-fonts homebrew-eid) + +if [[ ! $(which 'ghi') ]] || ! security find-internet-password -s github.com -l 'ghi token' &> /dev/null; then + echo -e "$(tput setaf 1) + This script requires 'ghi' installed and configured. + If you have [Homebrew](http://brew.sh), you can install it with 'brew install ghi'. + To configure it, run 'ghi config --auth '. Your Github password will be required, but is never stored. + $(tput sgr0)" | sed -E 's/ {4}//' >&2 + exit 1 +fi + +if [[ ! $(which 'fastmerge') ]]; then + echo -e "$(tput setaf 1) + This script requires 'fastmerge'. + If you have [Homebrew](http://brew.sh), you can install it with 'brew install vitorgalvao/tiny-scripts/fastmerge'. + $(tput sgr0)" | sed -E 's/ {4}//' >&2 + exit 1 +fi + +function message { + echo "${1}" +} + +function go_to_repos_dir { + [[ ! -d "${caskroom_repos_dir}" ]] && mkdir -p "${caskroom_repos_dir}" + cd "${caskroom_repos_dir}" || exit 1 +} + +function go_to_repo_and_update { + local repo_name repo_dir casks_dir + + repo_name="${1}" + repo_dir="${caskroom_repos_dir}/${repo_name}" + casks_dir="${repo_dir}/Casks" + + if [[ ! -d "${repo_dir}" ]]; then + go_to_repos_dir + + message "Cloning ${repo_name}…" + git clone "https://github.com/caskroom/${repo_name}.git" --quiet + + cd "${casks_dir}" || exit 1 + else + cd "${casks_dir}" || exit 1 + + message "Updating ${repo_name}…" + git pull --rebase origin master --quiet + fi +} + +function delete_current_branch { + local current_branch + + current_branch="$(git rev-parse --abbrev-ref HEAD)" + git checkout master --quiet + git branch -D "${current_branch}" --quiet +} + +function delete_cask_repair_branches { + [[ $(ghi list --state open --pulls --label 'outdated appcast' | tail -1) == 'None.' ]] && cask-repair --push origin --delete-branches +} + +function merge_outdated_appcasts { + local repo_name pr_number cask_name pr_url last_commit + + repo_name="${1}" + + for line in $(ghi list --state open --pulls --label 'outdated appcast' --reverse | tail +2); do + [[ "${line}" == 'None.' ]] && break # exit early if there are no relevant issues in repo + + pr_number="$(awk '{print $1}' <<< "${line}")" + cask_name="$(awk '{print $3}' <<< "${line}")" + pr_url="${caskroom_online}/${repo_name}/pull/${pr_number}" + + hub checkout "${pr_url}" &>/dev/null + last_commit="$(git log -n 1 --pretty=format:'%H')" + delete_current_branch + + if [[ "$(hub ci-status "${last_commit}")" == 'success' ]]; then + message "Merging pull request for ${cask_name}…" + fastmerge --maintainer --remote origin "${pr_url}" + else + continue + fi + done +} + +for repo in "${caskroom_repos[@]}"; do + go_to_repo_and_update "${repo}" + merge_outdated_appcasts "${repo}" + delete_cask_repair_branches + git gc +done diff --git a/Library/Homebrew/cask/developer/bin/production_brew_cask b/Library/Homebrew/cask/developer/bin/production_brew_cask new file mode 120000 index 0000000000..4afaf77a65 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/production_brew_cask @@ -0,0 +1 @@ +develop_brew_cask \ No newline at end of file diff --git a/Library/Homebrew/cask/developer/bin/project_stats b/Library/Homebrew/cask/developer/bin/project_stats new file mode 100755 index 0000000000..652b6d7b33 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/project_stats @@ -0,0 +1,234 @@ +#!/bin/bash +# +# project_stats +# +# stats on project/release from git database +# + +### +### settings +### + +set -e # exit on any uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### configurable global variables +### + +# these paths relative to project root +declare -a cask_paths=(Casks) +declare -a code_paths=(bin developer lib spec test brew-cask.rb Rakefile Gemfile Gemfile.lock .travis.yml .gitignore) +declare -a doc_paths=(doc LICENSE "*.md") +end_object="HEAD" + +### +### global variables +### + +cask_authors='' + +# prefer GNU xargs +xargs="$(/usr/bin/which gxargs || printf '/usr/bin/xargs')" + +### +### functions +### + +warn () { + local message="$@" + message="${message//\\t/$'\011'}" + message="${message//\\n/$'\012'}" + message="${message%"${message##*[![:space:]]}"}" + printf "%s\n" "$message" 1>&2 +} + +die () { + warn "$@" + exit 1 +} + +cd_to_project_root () { + local script_dir="$(/usr/bin/dirname "$0")" + cd "$script_dir" + local git_root="$(git rev-parse --show-toplevel)" + if [[ -z "$git_root" ]]; then + die "ERROR: Could not find git project root" + fi + cd "$git_root" +} + +warn_if_off_branch () { + local wanted_branch='master' + if [[ -n "$1" ]]; then + wanted_branch="$1" + fi + + local current_branch="$(git rev-parse --abbrev-ref HEAD)" + if ! [[ "$current_branch" = "$wanted_branch" ]]; then + warn "\nWARNING: you are running from branch '$current_branch', not '$wanted_branch'\n\n" + fi +} + +verify_git_object () { + local object="$1" + + if ! git rev-parse --verify "$object" -- >/dev/null 2>&1; then + die "\nERROR: No such commit object: '$object'\n\n" + fi +} + +print_contributor_stats () { + local start_object="$1" + local initial_commit="$2" + + printf "====================\n" + printf "Contributors\n" + printf "====================\n" + + local -a git_log_cmd=("git" "log" "--no-merges" "--format='%ae'" "${start_object}..${end_object}") + printf "Unique contributors" + if ! [[ "$start_object" = "$initial_commit" ]]; then + printf " since %s" "${start_object#v}" + fi + printf "\n" + cask_authors="$("${git_log_cmd[@]}" -- "${cask_paths[@]}" | /usr/bin/sort | /usr/bin/uniq | /usr/bin/wc -l)" + printf " Casks\t%s\n" "$cask_authors" + printf " code\t" + "${git_log_cmd[@]}" -- "${code_paths[@]}" | /usr/bin/sort | /usr/bin/uniq | /usr/bin/wc -l + printf " docs\t" + "${git_log_cmd[@]}" -- "${doc_paths[@]}" | /usr/bin/sort | /usr/bin/uniq | /usr/bin/wc -l + printf " any\t" + "${git_log_cmd[@]}" -- . | /usr/bin/sort | /usr/bin/uniq | /usr/bin/wc -l + if ! [[ "$start_object" = "$initial_commit" ]]; then + local alltime_contribs="$(git log --no-merges --format='%ae' "${initial_commit}".."${end_object}" -- . | /usr/bin/sort | /usr/bin/uniq | /usr/bin/wc -l)" + local prior_contribs="$(git log --no-merges --format='%ae' "${initial_commit}".."${start_object}" -- . | /usr/bin/sort | /usr/bin/uniq | /usr/bin/wc -l)" + # arithmetic removes whitespace + ((alltime_contribs += 0)) + ((new_contribs = alltime_contribs - prior_contribs)) + printf "\nAll-time contributors\t%s\n" "$alltime_contribs" + printf "New contributors since %s\t%s\n" "${start_object#v}" "$new_contribs" + fi + printf "\n" +} + +print_commit_stats () { + local start_object="$1" + local initial_commit="$2" + + printf "====================\n" + printf "Commits\n" + printf "====================\n" + + local -a git_log_cmd=("git" "log" "--no-merges" "--format='%ae'" "${start_object}..${end_object}") + printf "Commit count" + if ! [[ "$start_object" = "$initial_commit" ]]; then + printf " since %s" "${start_object#v}" + fi + printf "\n" + printf " Casks\t" + "${git_log_cmd[@]}" -- "${cask_paths[@]}" | /usr/bin/wc -l + printf " code\t" + "${git_log_cmd[@]}" -- "${code_paths[@]}" | /usr/bin/wc -l + printf " docs\t" + "${git_log_cmd[@]}" -- "${doc_paths[@]}" | /usr/bin/wc -l + printf " any\t" + "${git_log_cmd[@]}" -- . | /usr/bin/wc -l + if ! [[ "$start_object" = "$initial_commit" ]]; then + printf "\nAll-time commits\t" + git log --no-merges --format='%ae' "${initial_commit}".."${end_object}" -- . | /usr/bin/wc -l + fi + printf "\n" +} + +print_doc_stats () { + local start_object="$1" + local initial_commit="$2" + + printf "====================\n" + printf "Docs\n" + printf "====================\n" + + local -a git_log_cmd=("git" "log" "--no-merges" "--format='%ae'" "${start_object}..${end_object}") + printf "Doc contributors" + if ! [[ "$start_object" = "$initial_commit" ]]; then + printf " since %s" "${start_object#v}" + fi + printf "\n " + "${git_log_cmd[@]}" -- "${doc_paths[@]}" | /usr/bin/sort | /usr/bin/uniq | \ + /usr/bin/egrep -v $'^\'(paul\\.t\\.hinze@gmail\\.com|fanquake@users\\.noreply\\.github\\.com|fanquake@gmail\\.com|info@vitorgalvao\\.com|calebcenter@live\\.com|hagins\\.josh@gmail\\.com|dragon\\.vctr@gmail\\.com|github@adityadalal\\.com|adityadalal924@users\\.noreply\\.github\\.com)\'$' | \ + "$xargs" | /usr/bin/perl -pe 's{ }{, }g' # ' + printf "\n" +} + +print_cask_stats () { + local start_object="$1" + local initial_commit="$2" + + printf "====================\n" + printf "Casks\n" + printf "====================\n" + + if ! [[ "$start_object" = "$initial_commit" ]]; then + local new_casks="$(git diff --name-status "$start_object" "$end_object" -- "${cask_paths[@]}" | /usr/bin/grep '^A.*\.rb' | cut -f2 | /usr/bin/sort | /usr/bin/uniq | /usr/bin/wc -l)" + local deleted_casks="$(git diff --name-status "$start_object" "$end_object" -- "${cask_paths[@]}" | /usr/bin/grep '^D.*\.rb' | cut -f2 | /usr/bin/sort | /usr/bin/uniq | /usr/bin/wc -l)" + local updated_casks="$(git diff --name-status "$start_object" "$end_object" -- "${cask_paths[@]}" | /usr/bin/grep '^M.*\.rb' | cut -f2 | /usr/bin/sort | /usr/bin/uniq | /usr/bin/wc -l)" + # arithmetic removes whitespace + ((cask_authors += 0)) + ((deleted_casks += 0)) + ((new_casks -= deleted_casks)) + ((updated_casks += 0)) + printf "%s Casks added (%s updated) by %s contributors since %s\n" "$new_casks" "$updated_casks" "$cask_authors" "${start_object#v}" + fi + + printf "Total current Casks in HEAD\t" + /usr/bin/find "${cask_paths[@]}" -name '*.rb' | /usr/bin/wc -l + printf "\n" +} + +### +### main +### + +_project_stats () { + local arg_object="$1" + + cd_to_project_root + warn_if_off_branch 'master' + + local initial_commit="$(git log --pretty=format:%H -- | /usr/bin/tail -1)" + verify_git_object "$initial_commit" + local start_object="$initial_commit" + + if [[ "$arg_object" = 'release' ]]; then + start_object="$(./developer/bin/get_release_tag)" + elif [[ -n "$arg_object" ]]; then + start_object="$arg_object" + fi + verify_git_object "$start_object" + + print_contributor_stats "$start_object" "$initial_commit" + print_commit_stats "$start_object" "$initial_commit" + print_doc_stats "$start_object" "$initial_commit" + print_cask_stats "$start_object" "$initial_commit" +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ ]]; then + printf "project_stats [ ] + +With optional single argument, (eg a tag or commit-hash) +show statistics since that commit object. + +Use the special argument 'release' to calculate since the +most recent tag (usually the same as the last release). + +Without argument, show statistics since first commit. + +" + exit +fi + +# dispatch main +_project_stats "${@}" diff --git a/Library/Homebrew/cask/developer/bin/the_long_tail b/Library/Homebrew/cask/developer/bin/the_long_tail new file mode 100755 index 0000000000..2a4cb8e7c3 --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/the_long_tail @@ -0,0 +1,252 @@ +#!/usr/bin/env ruby +# +# the_long_tail +# +# A histogram view on contributor stats +# +# notes +# +# Since this script does not track file-renames in the git history, the +# dependence of Casks upon occasional contributors/non-maintainers can +# only be expressed as a range or lower bound. +# + +### +### dependencies +### + +require "open3" +require "set" + +### +### configurable constants +### + +BINS = [ + (1..10).to_a, + 100, + 1000, + ].flatten + +OCCASIONAL_CUTOFF = 5 + +CASK_PATH = "Casks".freeze + +# all maintainers, past and present +MAINTAINERS = %w[ + paul.t.hinze@gmail.com + fanquake@users.noreply.github.com + fanquake@gmail.com + kevin@suttle.io + leoj3n@gmail.com + nano@fdp.io + nanoid.xd@gmail.com + me@passcod.name + walker@pobox.com + info@vitorgalvao.com + calebcenter@live.com + ndr@qef.io + josh@joshbutts.com + goxberry@gmail.com + radek.simko@gmail.com + federicobond@gmail.com + claui@users.noreply.github.com + amorymeltzer@gmail.com + hagins.josh@gmail.com + dragon.vctr@gmail.com + mail@sebastianroeder.de + github@adityadalal.com + adityadalal924@users.noreply.github.com + ].freeze + +### +### git methods +### + +def cd_to_project_root + Dir.chdir File.dirname(File.expand_path(__FILE__)) + @git_root ||= Open3.popen3(*%w[ + git rev-parse --show-toplevel + ]) do |_stdin, stdout, _stderr| + begin + stdout.gets.chomp + rescue + end + end + Dir.chdir @git_root + @git_root +end + +def authors + @authors ||= Open3.popen3(*%w[ + git log --no-merges --format=%ae -- + ]) do |_stdin, stdout, _stderr| + h = {} + stdout.each_line do |line| + line.chomp! + h[line] ||= 0 + h[line] += 1 + end + h + end +end + +def casks_by_author + @casks_by_author ||= Open3.popen3(*%w[ + git log --no-merges --name-only --format=%ae -- + ], + CASK_PATH) do |_stdin, stdout, _stderr| + email = nil + h = {} + stdout.each_line.to_a.join("").split("\n\n").each do |paragraph| + if paragraph.include?("Casks/") + lines = paragraph.split("\n") + email = lines.pop + h[email] ||= Set.new + h[email].merge(lines.compact) + else + email = paragraph.chomp + end + end + h + end +end + +### +### filesystem methods +### + +def all_casks + @all_casks ||= Open3.popen2("/usr/bin/find", + CASK_PATH, + *%w[-type f -name *.rb]) do |_stdin, stdout| + stdout.each_line.map(&:chomp) + end +end + +### +### analysis and report methods +### + +def histogram + if @histogram.nil? + @histogram = Hash[*BINS.map { |elt| [elt, 0] }.flatten] + authors.each do |_name, num_commits| + bottom = 0 + BINS.each do |top| + @histogram[bottom] += 1 if num_commits >= bottom && num_commits < top + bottom = top + end + end + end + @histogram +end + +def historic_occasional_cask_set + @historic_occasional_cask_set = authors.each.collect do |name, num_commits| + if num_commits > OCCASIONAL_CUTOFF + nil + elsif !casks_by_author.key?(name) + nil + else + casks_by_author[name].to_a + end + end.flatten.compact.to_set +end + +def extant_occasional_cask_count + # avoid double-counting renames by intersecting with extant Casks + historic_occasional_cask_set.intersection(all_casks).count +end + +def historic_nonmaintainer_cask_set + @historic_nonmaintainer_cask_set = authors.each.collect do |name, _num_commits| + if MAINTAINERS.include?(name) + nil + else + casks_by_author[name].to_a + end + end.flatten.compact.to_set +end + +def extant_nonmaintainer_cask_count + # avoid double-counting renames by intersecting with extant Casks + historic_nonmaintainer_cask_set.intersection(all_casks).count +end + +def extant_occasional_cask_percentage + @extant_occasional_cask_percentage ||= (100 * extant_occasional_cask_count / all_casks.count).to_i +end + +def historic_occasional_cask_percentage + @historic_occasional_cask_percentage ||= (100 * historic_occasional_cask_set.count / all_casks.count).to_i +end + +def extant_nonmaintainer_cask_percentage + @extant_nonmaintainer_cask_percentage ||= (100 * extant_nonmaintainer_cask_count / all_casks.count).to_i +end + +def historic_nonmaintainer_cask_percentage + # this is so large, it might cross 100% + @historic_nonmaintainer_cask_percentage ||= [100, (100 * historic_nonmaintainer_cask_set.count / all_casks.count).to_i].min +end + +def onetime_author_percentage + @onetime_author_percentage ||= (100 * + histogram[1] / + authors.length).to_i +end + +def occasional_author_percentage + # why is it so hard to slice a hash? + @occasional_author_percentage ||= (100 * + (1..OCCASIONAL_CUTOFF).to_a.collect { |bin| histogram[bin] }.reduce(:+) / + authors.length).to_i +end + +def graph_width + if @graph_width.nil? + @graph_width = `/bin/stty size 2>/dev/null`.chomp.split(" ").last.to_i + @graph_width = 80 if @graph_width <= 0 + @graph_width -= 20 if @graph_width > 20 + end + @graph_width +end + +def graph_normalization + @graph_normalization ||= histogram.values.max.to_f +end + +def print_header + puts "Commits\tContributors" + puts "---------------------" +end + +def print_table + BINS.each do |bin| + plural = (bin % 10) == 0 ? "'s" : "" + graph = "." * ((histogram[bin] / graph_normalization) * graph_width) + puts "#{bin}#{plural}\t#{histogram[bin]}\t#{graph}" + end +end + +def print_footer + puts %Q{\n#{occasional_author_percentage}% of contributors are "occasional" (with <= #{OCCASIONAL_CUTOFF} commits)} + puts "\n#{onetime_author_percentage}% of contributors commit only once" + puts "\n#{extant_occasional_cask_percentage}% - #{historic_occasional_cask_percentage}% of Casks depend on an occasional contributor" + puts "\n#{extant_nonmaintainer_cask_percentage}% - #{historic_nonmaintainer_cask_percentage}% of Casks depend on a contributor who is not a maintainer" + puts "\n" +end + +def generate_report + print_header + print_table + print_footer +end + +### +### main +### + +cd_to_project_root +generate_report diff --git a/Library/Homebrew/cask/developer/bin/update_issue_template_urls b/Library/Homebrew/cask/developer/bin/update_issue_template_urls new file mode 100755 index 0000000000..8e61ff69ec --- /dev/null +++ b/Library/Homebrew/cask/developer/bin/update_issue_template_urls @@ -0,0 +1,106 @@ +#!/usr/bin/env bash +# +# update_issue_template_urls +# + +### +### settings +### + +set -e # exit on uncaught error +set +o histexpand # don't expand history expressions +shopt -s nocasematch # case-insensitive regular expressions + +### +### constants +### + +script_subdir='developer/bin' +template_subdir='doc/issue_templates' +generate_url_script='generate_issue_template_urls' +files_to_update=('README.md') + +### +### functions +### + +warn () { + local message="$*" + message="${message//\\t/$'\011'}" + message="${message//\\n/$'\012'}" + message="${message%${message##*[![:space:]]}}" + printf "%s\n" "$message" 1>&2 +} + +die () { + warn "$@" + exit 1 +} + +usage () { + printf "update_issue_template_urls + +Regenerate issue template URLs and update them in relevant files. + +Note: docs using issue template URLs must use a specific format. +If the template file is called 'bug_report.md', the URL must be +referenced in the doc as follows: + + [Some link text][bug_report_template] + + ... + + [bug_report_template]: (auto-generated-url) + +" +} + +cd_to_project_root () { + local script_dir git_root + script_dir="$(/usr/bin/dirname "$0")" + cd "$script_dir" + git_root="$(git rev-parse --show-toplevel)" + if [[ -z "$git_root" ]]; then + die "ERROR: Could not find git project root" + fi + cd "$git_root" +} + +generate_template_url () { + local template_file="$1" + "$script_subdir/$generate_url_script" "$template_file" +} + +update_template_url () { + local template_name="$1" + local template_url="$2" + local escaped_template_url="${template_url/&/\\&}" + + /usr/bin/sed -i '' \ + -e "s|^\\(\\[${template_name}_template\\]: \\).*$|\\1${escaped_template_url}|g" \ + -- "${files_to_update[@]}" +} + +### +### main +### + +_update_issue_template_urls () { + local template_file template_name template_url + cd_to_project_root + for template_file in ./$template_subdir/*; do + template_name="${template_file##*/}" + template_name="${template_name%%.*}" + template_url="$(generate_template_url "$template_file")" + update_template_url "$template_name" "$template_url" + done +} + +# process args +if [[ $1 =~ ^-+h(elp)?$ ]]; then + usage + exit +fi + +# dispatch main +_update_issue_template_urls diff --git a/Library/Homebrew/cask/developer/examples/brewcask-doutdated.rb b/Library/Homebrew/cask/developer/examples/brewcask-doutdated.rb new file mode 100755 index 0000000000..de0234c2f1 --- /dev/null +++ b/Library/Homebrew/cask/developer/examples/brewcask-doutdated.rb @@ -0,0 +1,43 @@ +#!/usr/bin/env ruby +# +# Generously contributed by Markus Doits +# https://github.com/doits +# (c) 2014 MIT license +# + +require "rubygems" + +class Hbc + def installed_version? + !installed_version.nil? + end + + def installed_version + # returns latest installed version if possible + + Pathname.glob(caskroom_path.join("*")).map(&:basename).sort do |x, y| + Gem::Version.new(x) <=> Gem::Version.new(y) # throws exception if invalid version is provided ... + end.last + rescue + nil + # ... return nil in this case + end + + def update_available? + Gem::Version.correct?(version) && # we have something to compare against in Cask file ... + installed_version? && # ... we can determine current installed version ... + Gem::Version.new(installed_version) < Gem::Version.new(version) # ... compare + end +end + +module Hbc::Scopes + module ClassMethods + def upgradable + Hbc.installed.select(&:update_available?) + end + end +end + +upgradable_casks = Hbc.upgradable + +puts upgradable_casks.empty? && "No outdated packages" || upgradable_casks diff --git a/Library/Homebrew/cask/developer/examples/brewcask-dumpcask.rb b/Library/Homebrew/cask/developer/examples/brewcask-dumpcask.rb new file mode 100755 index 0000000000..8e209f9415 --- /dev/null +++ b/Library/Homebrew/cask/developer/examples/brewcask-dumpcask.rb @@ -0,0 +1,16 @@ +# brewcask-dumpcask +# +# A trivial `brew cask` external command, implemented in Ruby. +# Loads a Cask definition, then dumps it in YAML format. +# Example usage: +# +# brew cask dumpcask google-chrome +# + +command_name = ARGV.shift +cask_token = ARGV.shift + +cask = Hbc.load(cask_token) + +Hbc.debug = true +cask.dumpcask diff --git a/Library/Homebrew/cask/developer/examples/brewcask-showargs b/Library/Homebrew/cask/developer/examples/brewcask-showargs new file mode 100755 index 0000000000..9f9d06dcbc --- /dev/null +++ b/Library/Homebrew/cask/developer/examples/brewcask-showargs @@ -0,0 +1,16 @@ +#!/bin/bash +# +# brewcask-showargs +# +# A trivial `brew cask` external command, implemented in bash. +# Displays the arguments passed to it. Example usage: +# +# brew cask showargs these were my args +# + +set -e; # exit on uncaught error +set +o histexpand; # don't expand history expressions + +echo "$@"; + +# diff --git a/Library/Homebrew/cask/doc/cask_language_reference/all_stanzas.md b/Library/Homebrew/cask/doc/cask_language_reference/all_stanzas.md new file mode 100644 index 0000000000..63930b8bfb --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/all_stanzas.md @@ -0,0 +1,59 @@ +# All stanzas + +## Required Stanzas + +Each of the following stanzas is required for every Cask. + +| name | multiple occurrences allowed? | value | +| ------------------ |------------------------------ | ----------- | +| `version` | no | application version; give value of `:latest` if versioned downloads are not offered +| `sha256` | no | SHA-256 checksum of the file downloaded from `url`, calculated by the command `shasum -a 256 `. Can be suppressed by using the special value `:no_check` (see also [Checksum Stanza Details](stanzas/sha256.md)) +| `url` | no | URL to the `.dmg`/`.zip`/`.tgz`/`.tbz2` file that contains the application.
A [comment](stanzas/url.md#when-url-and-homepage-hostnames-differ-add-a-comment) should be added if the hostnames in the `url` and `homepage` stanzas differ. Block syntax should be used for URLs that change on every visit.
See [URL Stanza Details](stanzas/url.md) for more information. +| `name` | yes | a string providing the full and proper name defined by the vendor (see also [Name Stanza Details](stanzas/name.md)) +| `homepage` | no | application homepage; used for the `brew cask home` command +| `license` | no | a symbol identifying the license category for the application (see also [License Stanza Details](stanzas/license.md)) + +## At Least One Artifact Stanza Is Also Required + +Each Cask must declare one or more *artifacts* (i.e. something to install). + +| name | multiple occurrences allowed? | value | +| ------------------ |------------------------------ | ----------- | +| `app` | yes | relative path to an `.app` that should be moved into the `/Applications` folder on installation (see also [App Stanza Details](stanzas/app.md)) +| `pkg` | yes | relative path to a `.pkg` file containing the distribution (see also [Pkg Stanza Details](stanzas/pkg.md)) +| `binary` | yes | relative path to a Binary that should be linked into the `/usr/local/bin` folder on installation +| `colorpicker` | yes | relative path to a ColorPicker plugin that should be linked into the `~/Library/ColorPickers` folder on installation +| `font` | yes | relative path to a Font that should be linked into the `~/Library/Fonts` folder on installation +| `input_method` | yes | relative path to a Input Method that should be linked into the `~/Library/Input Methods` folder on installation +| `internet_plugin` | yes | relative path to a Service that should be linked into the `~/Library/Internet Plug-Ins` folder on installation +| `prefpane` | yes | relative path to a Preference Pane that should be linked into the `~/Library/PreferencePanes` folder on installation +| `qlplugin` | yes | relative path to a QuickLook Plugin that should be linked into the `~/Library/QuickLook` folder on installation +| `screen_saver` | yes | relative path to a Screen Saver that should be linked into the `~/Library/Screen Savers` folder on installation +| `service` | yes | relative path to a Service that should be linked into the `~/Library/Services` folder on installation +| `audio_unit_plugin`| yes | relative path to an Audio Unit plugin that should be linked into the `~/Library/Audio/Components` folder on installation +| `vst_plugin` | yes | relative path to a VST Plugin that should be linked into the `~/Library/Audio/VST` folder on installation +| `vst3_plugin` | yes | relative path to a VST3 Plugin that should be linked into the `~/Library/Audio/VST3` folder on installation +| `suite` | yes | relative path to a containing directory that should be moved into the `/Applications` folder on installation (see also [Suite Stanza Details](stanzas/suite.md)) +| `artifact` | yes | relative path to an arbitrary path that should be symlinked on installation. Must provide an absolute path as a `target` (example [alcatraz.rb](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/alcatraz.rb#L12)). This is only for unusual cases. The `app` stanza is strongly preferred when linking `.app` bundles. +| `installer` | yes | describes an executable which must be run to complete the installation (see [Installer Stanza Details](stanzas/installer.md)) +| `stage_only` | no | `true`. Assert that the Cask contains no activatable artifacts. + +## Optional Stanzas + +| name | multiple occurrences allowed? | value | +| ---------------------- |------------------------------ | ----------- | +| `uninstall` | yes | procedures to uninstall a Cask. Optional unless the `pkg` stanza is used. (see also [Uninstall Stanza Details](stanzas/uninstall.md)) +| `zap` | yes | additional procedures for a more complete uninstall, including user files and shared resources (see also [Zap Stanza Details](stanzas/zap.md)) +| `appcast` | no | a URL providing an appcast feed to find updates for this Cask (see also [Appcast Stanza Details](stanzas/appcast.md)) +| `depends_on` | yes | a list of dependencies and requirements for this Cask (see also [Depends_on Stanza Details](stanzas/depends_on.md)) +| `conflicts_with` | yes | a list of conflicts with this Cask (*not yet functional* see also [Conflicts_with Stanza Details](stanzas/conflicts_with.md)) +| `caveats` | yes | a string or Ruby block providing the user with Cask-specific information at install time (see also [Caveats Stanza Details](stanzas/caveats.md)) +| `preflight` | yes | a Ruby block containing preflight install operations (needed only in very rare cases) +| `postflight` | yes | a Ruby block containing postflight install operations (see also [Postflight Stanza Details](stanzas/flight.md)) +| `uninstall_preflight` | yes | a Ruby block containing preflight uninstall operations (needed only in very rare cases) +| `uninstall_postflight` | yes | a Ruby block containing postflight uninstall operations +| `accessibility_access` | no | `true` if the application should be granted accessibility access +| `container nested:` | no | relative path to an inner container that must be extracted before moving on with the installation; this allows us to support dmg inside tar, zip inside dmg, etc. +| `container type:` | no | a symbol to override container-type autodetect. May be one of: `:air`, `:bz2`, `:cab`, `:dmg`, `:generic_unar`, `:gzip`, `:otf`, `:pkg`, `:rar`, `:seven_zip`, `:sit`, `:tar`, `:ttf`, `:xar`, `:zip`, `:naked`. (example [parse.rb](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/parse.rb#L11)) +| `gpg` | no | *stub: not yet functional.* (see also [GPG Stanza Details](stanzas/gpg.md)) +| `auto_updates` | no | `true`. Assert the Cask artifacts auto-update. (Use if `Check for Updates…` or similar is present in app menu) diff --git a/Library/Homebrew/cask/doc/cask_language_reference/readme.md b/Library/Homebrew/cask/doc/cask_language_reference/readme.md new file mode 100644 index 0000000000..1fcff78537 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/readme.md @@ -0,0 +1,175 @@ +# Synopsis + +## Casks Are Ruby Blocks + +Each Cask is a Ruby block, beginning with a special header line. The Cask definition itself is always enclosed in a `do … end` block. Example: + +```ruby +cask 'alfred' do + version '2.7.1_387' + sha256 'a3738d0513d736918a6d71535ef3d85dd184af267c05698e49ac4c6b48f38e17' + + url "https://cachefly.alfredapp.com/Alfred_#{version}.zip" + name 'Alfred' + homepage 'https://www.alfredapp.com/' + license :freemium + + app 'Alfred 2.app' + app 'Alfred 2.app/Contents/Preferences/Alfred Preferences.app' + + postflight do + suppress_move_to_applications key: 'suppressMoveToApplications' + end +end +``` + +## The Cask Language Is Declarative + +Each Cask contains a series of stanzas (or “fields”) which *declare* how the software is to be obtained and installed. In a declarative language, the author does not need to worry about **order**. As long as all the needed fields are present, Homebrew-Cask will figure out what needs to be done at install time. + +To make maintenance easier, the most-frequently-updated stanzas are usually placed at the top. But that’s a convention, not a rule. + +Exception: `do` blocks such as `postflight` may enclose a block of pure Ruby code. Lines within that block follow a procedural (order-dependent) paradigm. + +## Conditional Statements + +### Efficiency + +Conditional statements are permitted, but only if they are very efficient. +Tests on the following values are known to be acceptable: + +| value | examples +| ----------------------------|-------------------------------------- +| `MacOS.version` | [macports.rb](https://github.com/caskroom/homebrew-cask/blob/9eae0af0daf9b55f81a3af010cca3b0b1272e2db/Casks/macports.rb#L4#L20), [coconutbattery.rb](https://github.com/caskroom/homebrew-cask/blob/2c801af44be29fff7f3cb2996455fce5dd95d1cc/Casks/coconutbattery.rb#L3#L17) +| `Hardware::CPU.is_32_bit?` | [vuescan.rb](https://github.com/caskroom/homebrew-cask/blob/655bfe48b41ae94cb81b1003182b8de5fa2995ef/Casks/vuescan.rb#L5#L9) +| `Hardware::CPU.is_64_bit?` | none, see [Always Fall Through to the Newest Case](#always-fall-through-to-the-newest-case) + +### Version Comparisons + +Tests against `MacOS.version` may use either symbolic names or version +strings with numeric comparison operators: + +```ruby +if MacOS.version <= :mavericks # symbolic name +``` + +```ruby +if MacOS.version <= '10.9' # version string +``` + +The available symbols for macOS versions are: `:cheetah`, `:puma`, `:jaguar`, `:panther`, `:tiger`, `:leopard`, `:snow_leopard`, `:lion`, `:mountain_lion`, `:mavericks`, `:yosemite`, `:el_capitan`, and `:sierra`. The corresponding numeric version strings should given as major releases containing a single dot. + +### Always Fall Through to the Newest Case + +Conditionals should be constructed so that the default is the newest OS version or hardware type. When using an `if` statement, test for older versions, and then let the `else` statement hold the latest and greatest. This makes it more likely that the Cask will work without alteration when a new OS is released. Example (from [coconutbattery.rb](https://github.com/caskroom/homebrew-cask/blob/2c801af44be29fff7f3cb2996455fce5dd95d1cc/Casks/coconutbattery.rb)): + +```ruby +if MacOS.version <= :tiger + # ... +elsif MacOS.version <= :snow_leopard + # ... +else + # ... +end +``` + +## Arbitrary Ruby Methods + +In the exceptional case that the Cask DSL is insufficient, it is possible to define arbitrary Ruby variables and methods inside the Cask by creating a `Utils` namespace. Example: + +```ruby +cask 'myapp' do + module Utils + def self.arbitrary_method + ... + end + end + + name 'MyApp' + version '1.0' + sha256 'a32565cdb1673f4071593d4cc9e1c26bc884218b62fef8abc450daa47ba8fa92' + license :unknown + + url "https://#{Utils.arbitrary_method}" + homepage 'https://www.example.com/' + ... +end +``` + +This should be used sparingly: any method which is needed by two or more Casks should instead be rolled into the core. Care must also be taken that such methods be very efficient. + +Variables and methods should not be defined outside the `Utils` namespace, as they may collide with Homebrew-Cask internals. + +## Header Line Details + +The first non-comment line in a Cask follows the form: + +```ruby +cask '' do +``` + +[``](token_reference.md) should match the Cask filename, without the `.rb` extension, +enclosed in single quotes. + +The header line is not entirely strict Ruby: no comma is required after the Cask token. + +There are currently some arbitrary limitations on Cask tokens which are in the process of being removed. The Travis bot will catch any errors during the transition. + + +## Stanza order + +Having a common order for stanzas makes Casks easier to update and parse. Below is the the complete stanza sequence (no Cask will have all stanzas). The empty lines shown here are also important, as they help to visually delineate information. + +``` +version +sha256 + +url +appcast, + checkpoint: # shown here as it is required with `appcast` +name +homepage +license +gpg, key_id: # on same line, since first part is typically small + +auto_updates +accessibility_access +conflicts_with +depends_on +container + +suite +app +pkg +installer +binary +colorpicker +font +input_method +internet_plugin +prefpane +qlplugin +screen_saver +service +audio_unit_plugin +vst_plugin +vst3_plugin +artifact, target: # :target shown here as is required with `artifact` +stage_only + +preflight + +postflight + +uninstall_preflight + +uninstall_postflight + +uninstall + +zap + +caveats +``` + +Note that every stanza that has additional parameters (`:symbols` after a `,`) shall have them on separate lines, one per line, in alphabetical order. Exceptions are `gpg` and `target:` (when not applied to `url`) which typically consist of short lines. diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/app.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/app.md new file mode 100644 index 0000000000..8a0d50b7b4 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/app.md @@ -0,0 +1,43 @@ +# app + +In the simple case of a string argument to `app`, the source file is moved to the target `~/Applications` directory. For example: + +```ruby +app 'Alfred 2.app' +``` + +moves the source to: + +```bash +/Applications/Alfred 2.app +``` + +from a source file such as: + +```bash +/usr/local/Caskroom/alfred/2.8.2_431/Alfred 2.app +``` + +## Renaming the Target + +You can rename the target which appears in your `/Applications` directory by adding a `target:` key to `app`. Example (from [scala-ide.rb](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/scala-ide.rb#L21)): + +```ruby +app 'eclipse/Eclipse.app', target: 'Scala IDE.app' +``` + +## target: May Contain an Absolute Path + +If `target:` has a leading slash, it is interpreted as an absolute path. The containing directory for the absolute path will be created if it does not already exist. Example (from [manopen.rb](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/manopen.rb#L12)): + +```ruby +artifact 'openman.1', target: '/usr/local/share/man/man1/openman.1' +``` + +## target: Works on Most Artifact Types + +The `target:` key works similarly for most Cask artifacts, such as `app`, `binary`, `colorpicker`, `font`, `input_method`, `prefpane`, `qlplugin`, `service`, `suite`, and `artifact`. + +## target: Should Only Be Used in Select Cases + +Don’t use `target:` for aesthetic reasons, like removing version numbers (`app "Slack #{version}.app", target: 'Slack.app'`). With `app`, use it when it makes sense functionally and document your reason cleary in the Cask: was it [for clarity](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/imagemin.rb#L12); [for consistency](https://github.com/caskroom/homebrew-cask/blob/d2a6b26df69fc28c4d84d6f5198b2b652c2f414d/Casks/devonthink-pro-office.rb#L16); [to prevent conflicts](https://github.com/caskroom/homebrew-cask/blob/bd6dc1a64e0bdd35ba0e20789045ea023b0b6aed/Casks/flash-player-debugger.rb#L11#L12); [due to developer suggestion](https://github.com/caskroom/homebrew-cask/blob/ff3e9c4a6623af44b8a071027e8dcf3f4edfc6d9/Casks/kivy.rb#L12)? With `binary` you can take some extra liberties to be consistent with other command-line tools, like [changing case](https://github.com/caskroom/homebrew-cask/blob/6e4eb6ba58ca0d9e6d42a1d78856cc8a35cf5fce/Casks/diffmerge.rb#L11) or [removing an extension](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/filebot.rb#L12). diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/appcast.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/appcast.md new file mode 100644 index 0000000000..704116049a --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/appcast.md @@ -0,0 +1,23 @@ +# appcast + +The value of the `appcast` stanza is a string, holding the URL for an appcast which provides information on future updates. + +## Required Appcast Parameters + +| key | value | +| ------------- | ----------- | +| `checkpoint:` | a string holding a custom checksum of the most recent appcast which matches the current Cask versioning. Use `curl --compressed --location --user-agent 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_9_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.152 Safari/537.36' "{{appcast_url}}" | sed 's|[^<]*||g' | shasum --algorithm 256` to calculate it. + +Example: [`atom`](https://github.com/caskroom/homebrew-cask/blob/161f85b605e160ff96e7dd11732d85609e13dc51/Casks/atom.rb#L7L8) + +There are a few different ways the `appcast` can be determined: + + * If the app is distributed via GitHub releases, the `appcast` will be of the form `https://github.com/{{user}}/{{project}}/releases.atom`. (Example Cask: [`electron`](https://github.com/caskroom/homebrew-cask/blob/161f85b605e160ff96e7dd11732d85609e13dc51/Casks/electron.rb#L6L7)) + + * The popular update framework [Sparkle](https://sparkle-project.org/) generally uses the `SUFeedURL` property in `Contents/Info.plist` inside `.app` bundles. You can use the script [`find_sparkle_appcast`](https://github.com/caskroom/homebrew-cask/blob/master/developer/bin/find_sparkle_appcast) to add this automatically. (Example Cask: [`glyphs`](https://github.com/caskroom/homebrew-cask/blob/161f85b605e160ff96e7dd11732d85609e13dc51/Casks/glyphs.rb#L6L7)) + +* Sourceforge projects follow the form `https://downloads.sourceforge.net/projects/{{project_name}}/rss`. A more specific page can be used as needed, pointing to a specific directory structure: `https://sourceforge.net/projects/{{project_name}}/rss?path=/{{path_here}}`. (Example Cask: [`seashore`](https://github.com/caskroom/homebrew-cask/blob/bcff548278a6776fc57439603442a8b23c76bd8b/Casks/seashore.rb#L6L7)) + +* HockeyApp URLs are of the form `https://rink.hockeyapp.net/api/2/apps/HEXADECIMAL_STRING`. For the `appcast`, remove `` (ending up with `https://rink.hockeyapp.net/api/2/apps/HEXADECIMAL_STRING`. (Example Cask: [`iconjar`](https://github.com/caskroom/homebrew-cask/blob/bcff548278a6776fc57439603442a8b23c76bd8b/Casks/iconjar.rb#L7L8)) + +* An appcast can be any URL hosted by the app’s developer that changes every time a new release is out (e.g. a changelog HTML page). (Example Cask: [`shortcat`](https://github.com/caskroom/homebrew-cask/blob/161f85b605e160ff96e7dd11732d85609e13dc51/Casks/shortcat.rb#L6L7)) diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/caveats.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/caveats.md new file mode 100644 index 0000000000..a6b58e7e75 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/caveats.md @@ -0,0 +1,53 @@ +# caveats + +Sometimes there are particularities with the installation of a piece of software that cannot or should not be handled programatically by Homebrew-Cask. In those instances, `caveats` is the way to inform the user. Information in `caveats` is displayed when a cask is invoked with either `install` or `info`. + +To avoid flooding users with too many messages (thus desensitising them to the important ones), `caveats` should be used sparingly and exclusively for installation-related matters. If you’re not sure a `caveat` you find pertinent is installation-related or not, ask a maintainer. As a general rule, if your case isn’t already covered in our comprehensive [`caveats Mini-DSL`](#caveats-mini-dsl), it’s unlikely to be accepted. + +## caveats as a String + +When `caveats` is a string, it is evaluated at compile time. The following methods are available for interpolation if `caveats` is placed in its customary position at the end of the Cask: + +| method | description | +| ------------------ | ----------- | +| `token` | the Cask token +| `version` | the Cask version +| `homepage` | the Cask homepage +| `caskroom_path` | the containing directory for all staged Casks, typically `/usr/local/Caskroom` (only available with block form) +| `staged_path` | the staged location for this Cask, including version number, *eg* `/usr/local/Caskroom/adium/1.5.10` (only available with block form) + +Example: + +```ruby +caveats "Using #{token} is hazardous to your health." +``` + +## caveats as a Block + +When `caveats` is a Ruby block, evaluation is deferred until install time. Within a block you may refer to the `@cask` instance variable, and invoke any method available on `@cask`. + +## caveats Mini-DSL + +There is a mini-DSL available within `caveats` blocks. + +The following methods may be called to generate standard warning messages: + +| method | description | +| --------------------------------- | ----------- | +| `path_environment_variable(path)` | users should make sure `path` is in their `$PATH` environment variable +| `zsh_path_helper(path)` | zsh users must take additional steps to make sure `path` is in their `$PATH` environment variable +| `depends_on_java(version)` | users should make sure they have the specified version of java installed. `version` can be exact (e.g. `6`), a minimum (e.g. `7+`), or omitted (when any version works). +| `logout` | users should log out and log back in to complete installation +| `reboot` | users should reboot to complete installation +| `files_in_usr_local` | the Cask installs files to `/usr/local`, which may confuse Homebrew +| `discontinued` | all software development has been officially discontinued upstream +| `free_license(web_page)` | users may get an official license to use the software at `web_page` +| `malware(radar_number)` | app has been reported to bundle malware. See [the FAQ](https://github.com/caskroom/homebrew-cask/blob/master/doc/faq/apps_with_malware.md) for the necessary steps. + +Example: + +```ruby +caveats do + path_environment_variable '/usr/texbin' +end +``` diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/conflicts_with.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/conflicts_with.md new file mode 100644 index 0000000000..e28b30a4bf --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/conflicts_with.md @@ -0,0 +1,14 @@ +# conflicts_with + +`conflicts_with` is used to declare conflicts that keep a Cask from installing or working correctly. + +Several keys are accepted by `conflicts_with`, but none of them are yet enforced by the backend implementation. It is fine to proactively add `conflicts_with` stanzas to Casks in anticipation of future backend support; they are currently just a type of structured comment. + +| key | description | +| ---------- | ----------- | +| `formula:` | *stub - not yet functional* +| `cask:` | *stub - not yet functional* +| `macos:` | *stub - not yet functional* +| `arch:` | *stub - not yet functional* +| `x11:` | *stub - not yet functional* +| `java:` | *stub - not yet functional* diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/depends_on.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/depends_on.md new file mode 100644 index 0000000000..2b571c3110 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/depends_on.md @@ -0,0 +1,109 @@ +# depends_on + +`depends_on` is used to declare dependencies and requirements for a Cask. +`depends_on` is not consulted until `install` is attempted. + +## depends_on cask: + +The value should be another Cask token, needed by the current Cask. + +Example use: [`SSHFS`](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/sshfs.rb#L12) depends on OSXFUSE: + +```ruby +depends_on cask: 'osxfuse' +``` + +## depends_on formula: + +The value should name a Homebrew Formula needed by the Cask. + +Example use: some distributions are contained in archive formats such as `7z` which are not supported by stock Apple tools. For these cases, a more capable archive reader may be pulled in at install time by declaring a dependency on the Homebrew Formula `unar`: + +```ruby +depends_on formula: 'unar' +``` + +## depends_on macos: + +### Requiring an Exact macOS Release + +The value for `depends_on macos:` may be a symbol, string, or an array, listing the exact compatible macOS releases. + +The available values for macOS releases are: + +| symbol | corresponding string +| -------------------|---------------------- +| `:cheetah` | `'10.0'` +| `:puma` | `'10.1'` +| `:jaguar` | `'10.2'` +| `:panther` | `'10.3'` +| `:tiger` | `'10.4'` +| `:leopard` | `'10.5'` +| `:snow_leopard` | `'10.6'` +| `:lion` | `'10.7'` +| `:mountain_lion` | `'10.8'` +| `:mavericks` | `'10.9'` +| `:yosemite` | `'10.10'` +| `:el_capitan` | `'10.11'` +| `:sierra` | `'10.12'` + +Only major releases are covered (version numbers containing a single dot). The symbol form is preferred for readability. The following are all valid ways to enumerate the exact macOS release requirements for a Cask: + +```ruby +depends_on macos: :yosemite +depends_on macos: [:mavericks, :yosemite] +depends_on macos: '10.9' +depends_on macos: ['10.9', '10.10'] +``` + +### Setting a Minimum macOS Release + +`depends_on macos:` can also accept a string starting with a comparison operator such as `>=`, followed by an macOS release in the form above. The following are both valid expressions meaning “at least macOS 10.9”: + +```ruby +depends_on macos: '>= :mavericks' +depends_on macos: '>= 10.9' +``` + +A comparison expression cannot be combined with any other form of `depends_on macos:`. + +## depends_on arch: + +The value for `depends_on arch:` may be a symbol or an array of symbols, listing the hardware compatibility requirements for a Cask. The requirement is satisfied at install time if any one of multiple `arch:` value matches the user’s hardware. + +The available symbols for hardware are: + +| symbol | meaning | +| ---------- | -------------- | +| `:i386` | 32-bit Intel | +| `:x86_64` | 64-bit Intel | +| `:ppc_7400`| 32-bit PowerPC | +| `:ppc_64` | 64-bit PowerPC | +| `:intel` | Any Intel | +| `:ppc` | Any PowerPC | + +The following are all valid expressions: + +```ruby +depends_on arch: :x86_64 +depends_on arch: [:x86_64] # same meaning as above +depends_on arch: :intel +depends_on arch: [:i386, :x86_64] # same meaning as above +``` + +Since PowerPC hardware is no longer common, the expression most frequently needed will be: + +```ruby +depends_on arch: :x86_64 +``` + +## All depends_on Keys + +| key | description | +| ---------- | ----------- | +| `formula:` | a Homebrew Formula +| `cask:` | a Cask token +| `macos:` | a symbol, string, array, or comparison expression defining macOS release requirements +| `arch:` | a symbol or array defining hardware requirements +| `x11:` | a Boolean indicating a dependency on X11 +| `java:` | *stub - not yet functional* diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/flight.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/flight.md new file mode 100644 index 0000000000..b6c90b11e1 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/flight.md @@ -0,0 +1,24 @@ +# \*flight + +## Evaluation of Blocks is Always Deferred + +The Ruby blocks defined by `preflight`, `postflight`, `uninstall_preflight`, and `uninstall_postflight` are not evaluated until install time or uninstall time. Within a block, you may refer to the `@cask` instance variable, and invoke any method available on `@cask`. + +## \*flight Mini-DSL + +There is a mini-DSL available within these blocks. + +The following methods may be called to perform standard tasks: + +| method | availability | description | +| ----------------------------------------- | ------------------------------------------------ | ----------- | +| `plist_set(key, value)` | `preflight`, `postflight`, `uninstall_preflight` | set a value in the `Info.plist` file for the app bundle. Example: [`rubymine.rb`](https://github.com/caskroom/homebrew-cask/blob/c5dbc58b7c1b6290b611677882b205d702b29190/Casks/rubymine.rb#L12) +| `set_ownership(paths)` | `preflight`, `postflight`, `uninstall_preflight` | set user and group ownership of `paths`. Example: [`unifi-controller.rb`](https://github.com/caskroom/homebrew-cask/blob/8a452a41707af6a661049da6254571090fac5418/Casks/unifi-controller.rb#L13) +| `set_permissions(paths, permissions_str)` | `preflight`, `postflight`, `uninstall_preflight` | set permissions in `paths` to `permissions_str`. Example: [`docker-machine.rb`](https://github.com/caskroom/homebrew-cask/blob/8a452a41707af6a661049da6254571090fac5418/Casks/docker-machine.rb#L16) +| `suppress_move_to_applications` | `postflight` | suppress a dialog asking the user to move the app to the `/Applications` folder. Example: [`github.rb`](https://github.com/caskroom/homebrew-cask/blob/c5dbc58b7c1b6290b611677882b205d702b29190/Casks/github.rb#L13) + +`plist_set` currently has the limitation that it only operates on the bundle indicated by the first `app` stanza (and the Cask must contain an `app` stanza). + +`set_ownership(paths)` defaults user ownership to the current user and group ownership to `staff`. These can be changed by passing in extra options: `set_ownership(paths, user: 'user', group: 'group')`. + +`suppress_move_to_applications` optionally accepts a `:key` parameter for apps which use a nonstandard `defaults` key. Example: [`alfred.rb`](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/alfred.rb#L16). diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/gpg.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/gpg.md new file mode 100644 index 0000000000..cb0e3ef846 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/gpg.md @@ -0,0 +1,11 @@ +# gpg Stanza Details + +**This is a stub for upcoming functionality, and is not fully documented**. + +The `gpg` stanza contains signature information for GPG-signed distributions. The form is: + +```ruby +gpg , : +``` + +where `` is one of `key_id:` or `key_url:`, and `` points to the detached signature of the distribution. Commonly, the signature follows the `url` value. Example: [libreoffice.rb](https://github.com/caskroom/homebrew-cask/blob/42abacc85798d8c0b8d3f47c70b62ee65ce5ceaa/Casks/libreoffice.rb#L16#L17). diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/installer.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/installer.md new file mode 100644 index 0000000000..730771c0df --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/installer.md @@ -0,0 +1,33 @@ +# installer + +This stanza must always be accompanied by [`uninstall`](uninstall.md). + +The `installer` stanza takes a series of key-value pairs, the first key of which must be `manual:` or `script:`. + +## installer manual: + +`installer manual:` takes a single string value, describing a GUI installer which must be run by the user at a later time. The path may be absolute, or relative to the Cask. Example (from [little-snitch.rb](https://github.com/caskroom/homebrew-cask/blob/818047bf488be92923c8770ef3df8007a0db7704/Casks/little-snitch.rb#L10)): + +```ruby +installer manual: 'Little Snitch Installer.app' +``` + +## installer script: + +`installer script:` introduces a series of key-value pairs describing a command which will automate completion of the install. The form is similar to `uninstall script:`: + +| key | value +| ----------------|------------------------------ +| `script:` | path to an install script to be run via `sudo`. (Required first key.) +| `args:` | array of arguments to the install script +| `input:` | array of lines of input to be sent to `stdin` of the script +| `must_succeed:` | set to `false` if the script is allowed to fail +| `sudo:` | set to `false` if the script does not need `sudo` + +The path may be absolute, or relative to the Cask. Example (from [adobe-air.rb](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/adobe-air.rb#L10-#L12)): + +```ruby +installer script: 'Adobe AIR Installer.app/Contents/MacOS/Adobe AIR Installer', + args: %w[-silent], + sudo: true +``` diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/license.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/license.md new file mode 100644 index 0000000000..9739c3bdce --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/license.md @@ -0,0 +1,47 @@ +# license + +The `license` stanza is not free-form. A single value must be chosen from a list of valid symbols. + +The values for `license` are categories, rather than fully-specified licenses. For example, `:gpl` is a category; we do not distinguish between versions of the GPL. Similarly, `:cc` and `:bsd` comprise many variants. They must always pertain to the license of the software itself, not the vendor’s business model (a free app to access a paid service is still `:gratis`, not `:freemium`). + +The `license` stanza is intended as an aid to search/filtering of Casks. For full and complete information, the user must always rely on the vendor’s homepage. + +Note that `brew cask search` and `brew cask list` are not yet capable of using the information stored in the `license` stanza. + +## Generic Category Licenses + +Cask authors should use the most specific license category which is also correct. Generic categories are provided for difficult cases. `:unknown` is also perfectly fine if you are unsure. + +Example: [Chromium](https://www.chromium.org/chromium-os/licenses) includes code with multiple licenses, all of which are open source. Chromium licensing is described by the generic category [`:oss`](https://github.com/caskroom/homebrew-cask/blob/54a79f7dcceea9a922a5b608ac99466b9d10a191/Casks/chromium.rb#L7). + +| symbol | meaning | +| ----------- | ----------- | +| `:oss` | open-source software +| `:closed` | closed-source software +| `:unknown` | license unknown +| `:other` | license is known, but fits no category + +## Valid Licenses + +| symbol | generic category | meaning | URL | +| ---------------- | ---------------- | ------------------------------------------------------------------ | ----------- | +| `:gratis` | `:closed` | free-to-use, closed source | +| `:commercial` | `:closed` | not free to use | +| `:freemium` | `:closed` | free-to-use, payment required for full or additional functionality | +| `:affero` | `:oss` | Affero General Public License | +| `:apache` | `:oss` | Apache Public License | +| `:arphic` | `:oss` | Arphic Public License | +| `:artistic` | `:oss` | Artistic License | +| `:bsd` | `:oss` | BSD License | +| `:cc` | `:oss` | Creative Commons License | +| `:eclipse` | `:oss` | Eclipse Public License | +| `:gpl` | `:oss` | GNU Public License | +| `:isc` | `:oss` | Internet Systems Consortium License | +| `:lppl` | `:oss` | LaTeX Project Public License | +| `:ncsa` | `:oss` | University of Illinois/NCSA Open Source License | +| `:mit` | `:oss` | MIT License | +| `:mpl` | `:oss` | Mozilla Public License | +| `:ofl` | `:oss` | SIL Open Font License | +| `:public_domain` | `:oss` | not copyrighted | +| `:ubuntu_font` | `:oss` | Ubuntu Font License | +| `:x11` | `:oss` | X Consortium License | diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/name.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/name.md new file mode 100644 index 0000000000..115001ffbd --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/name.md @@ -0,0 +1,9 @@ +# name + +`name` accepts a UTF-8 string defining the full name of the software, and is used to help with searchability and disambiguation. It can be repeated multiple times if there are useful alternative names. + +Its first instance should use the latin alphabet, include the software vendor’s name, and be as verbose as possible while still making sense. + +A good example is [`pycharm-ce`](https://github.com/caskroom/homebrew-cask/blob/fc05c0353aebb28e40db72faba04b82ca832d11a/Casks/pycharm-ce.rb#L6#L7). `Jetbrains PyCharm Community Edition` makes sense even though it is likely never referenced as such anywhere, but `Jetbrains PyCharm Community Edition CE` doesn’t, hence why it has a second line. Another example are casks whose original names do not use the latin alphabet, like [`cave-story`](https://github.com/caskroom/homebrew-cask/blob/0fe48607f5656e4f1de58c6884945378b7e6f960/Casks/cave-story.rb#L7#L9). + +Note that `brew cask search` and `brew cask list` are not yet capable of using the information stored in the `name` stanza. \ No newline at end of file diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/pkg.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/pkg.md new file mode 100644 index 0000000000..0889cd2fab --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/pkg.md @@ -0,0 +1,19 @@ +# pkg + +This stanza must always be accompanied by [`uninstall`](uninstall.md) + +The first argument to the `pkg` stanza should be a relative path to the `.pkg` file to be installed. For example: + +```ruby +pkg 'Unity.pkg' +``` + +Subsequent arguments to `pkg` are key/value pairs which modify the install process. Currently supported keys are: + +* `allow_untrusted:` — pass `-allowUntrusted` to `/usr/sbin/installer` + +Example (from [alinof-timer.rb](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/alinof-timer.rb#L10)): + +```ruby +pkg 'AlinofTimer.pkg', allow_untrusted: true +``` diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/sha256.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/sha256.md new file mode 100644 index 0000000000..96d0115149 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/sha256.md @@ -0,0 +1,17 @@ +# sha256 + +## Calculating the SHA256 + +The `sha256` value is usually calculated by the command: + +```bash +$ shasum -a 256 +``` + +## Special Value `:no_check` + +The special value `sha256 :no_check` is used to turn off SHA checking whenever checksumming is impractical due to the upstream configuration. + +`version :latest` requires `sha256 :no_check`, and this pairing is common. However, `sha256 :no_check` does not require `version :latest`. + +We use a checksum whenever possible. \ No newline at end of file diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/suite.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/suite.md new file mode 100644 index 0000000000..024b3393f5 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/suite.md @@ -0,0 +1,11 @@ +# suite + +Some distributions provide a suite of multiple applications, or an application with required data, to be installed together in a subdirectory of `/Applications`. + +For these Casks, use the `suite` stanza to define the directory containing the application suite. Example (from [sketchup.rb](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/sketchup.rb#L12)): + +```ruby +suite 'SketchUp 2016' +``` + +The value of `suite` is never an `.app` bundle, but a plain directory. diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/uninstall.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/uninstall.md new file mode 100644 index 0000000000..94a1e3b02c --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/uninstall.md @@ -0,0 +1,199 @@ +# uninstall + +IF YOU CANNOT DESIGN A WORKING `UNINSTALL` STANZA, PLEASE SUBMIT YOUR CASK ANYWAY. The maintainers will help you write an `uninstall` stanza, just ask! + +## uninstall pkgutil: Is The Easiest and Most Useful + +`pkgutil:` is the easiest and most useful `uninstall` directive. See [Uninstall Key pkgutil:](#uninstall-key-pkgutil). + +## uninstall Is Required for Casks That Install a pkg or installer manual: + +For most Casks, uninstall actions are determined automatically, and an explicit `uninstall` stanza is not needed. However, a Cask which uses the `pkg` or `installer manual:` stanzas will **not** know how to uninstall correctly unless an `uninstall` stanza is given. + +So, while the Cask language does not enforce the requirement, it is much better for end-users if every `pkg` and `installer manual:` has a corresponding `uninstall`. + +The `uninstall` stanza is available for non-`pkg` Casks, and is useful for a few corner cases. However, the documentation below concerns the typical case of using `uninstall` to define procedures for a `pkg`. + +## There Are Multiple Uninstall Techniques + +Since `pkg` installers can do arbitrary things, different techniques are needed to uninstall in each case. You may need to specify one, or several, of the following key/value pairs as arguments to `uninstall`. + +## Summary of Keys + +* `early_script:` (string or hash) - like `script:`, but runs early (for special cases, best avoided) +* `launchctl:` (string or array) - ids of `launchctl` jobs to remove +* `quit:` (string or array) - bundle ids of running applications to quit +* `signal:` (array of arrays) - signal numbers and bundle ids of running applications to send a Unix signal to (used when `quit:` does not work) +* `login_item:` (string or array) - names of login items to remove +* `kext:` (string or array) - bundle ids of kexts to unload from the system +* `pkgutil:` (string, regexp or array of strings and regexps) - strings or regexps matching bundle ids of packages to uninstall using `pkgutil` +* `script:` (string or hash) - relative path to an uninstall script to be run via sudo; use hash if args are needed + - `executable:` - relative path to an uninstall script to be run via sudo (required for hash form) + - `args:` - array of arguments to the uninstall script + - `input:` - array of lines of input to be sent to `stdin` of the script + - `must_succeed:` - set to `false` if the script is allowed to fail + - `sudo:` - set to `false` if the script does not need `sudo` +* `delete:` (string or array) - single-quoted, absolute paths of files or directory trees to remove. `delete:` should only be used as a last resort. `pkgutil:` is strongly preferred. +* `rmdir:` (string or array) - single-quoted, absolute paths of directories to remove if empty +* `trash:` (string or array) - currently a synonym for `delete:`. In the future this will cause files to be moved to the Trash. + +Each `uninstall` technique is applied according to the order above. The order in which `uninstall` keys appear in the Cask file is ignored. + +For assistance filling in the right values for `uninstall` keys, there are several helper scripts found under `developer/bin` in the Homebrew-Cask repository. Each of these scripts responds to the `-help` option with additional documentation. + +The easiest way to work out an `uninstall` stanza is on a system where the `pkg` is currently installed and operational. To operate on an uninstalled `pkg` file, see [Working With a pkg File Manually](#working-with-a-pkg-file-manually), below. + +## uninstall Key pkgutil: + +This is the most useful uninstall key. `pkgutil:` is often sufficient to completely uninstall a `pkg`, and is strongly preferred over `delete:`. + +IDs for the most recently-installed packages can be listed using the command: + +```bash +$ ./developer/bin/list_recent_pkg_ids +``` + +`pkgutil:` also accepts a regular expression match against multiple package IDs. The regular expressions are somewhat nonstandard. To test a `pkgutil:` regular expression against currently-installed packages, use the command: + +```bash +$ ./developer/bin/list_pkg_ids_by_regexp +``` + +## List Files Associated With a pkg Id + +Once you know the ID for an installed package, (above), you can list all files on your system associated with that package ID using the macOS command: + +```bash +$ pkgutil --files +``` + +Listing the associated files can help you assess whether the package included any `launchctl` jobs or kernel extensions (kexts). + +## uninstall Key launchctl: + +IDs for currently loaded `launchctl` jobs can be listed using the command: + +```bash +$ ./developer/bin/list_loaded_launchjob_ids +``` + +IDs for all installed `launchctl` jobs can be listed using the command: + +```bash +$ ./developer/bin/list_installed_launchjob_ids +``` + +## uninstall Key quit: + +Bundle IDs for currently running Applications can be listed using the command: + +```bash +$ ./developer/bin/list_running_app_ids +``` + +Bundle IDs inside an Application bundle on disk can be listed using the command: + +```bash +$ ./developer/bin/list_ids_in_app +``` + +## uninstall Key signal: + +`signal:` should only be needed in the rare case that a process does not respond to `quit:`. + +Bundle IDs for `signal:` targets may be obtained as for `quit:`. The value for `signal:` is an array-of-arrays, with each cell containing two elements: the desired Unix signal followed by the corresponding bundle ID. + +The Unix signal may be given in numeric or string form (see the `kill` man page for more details). + +The elements of the `signal:` array are applied in order, only if there is an existing process associated the bundle ID, and stopping when that process terminates. A bundle ID may be repeated to send more than one signal to the same process. + +It is better to use the least-severe signals which are sufficient to stop a process. The `KILL` signal in particular can have unwanted side-effects. + +An example, with commonly-used signals in ascending order of severity: + +```ruby + uninstall signal: [ + ['TERM', 'fr.madrau.switchresx.daemon'], + ['QUIT', 'fr.madrau.switchresx.daemon'], + ['INT', 'fr.madrau.switchresx.daemon'], + ['HUP', 'fr.madrau.switchresx.daemon'], + ['KILL', 'fr.madrau.switchresx.daemon'], + ] +``` + +Note that when multiple running processes match the given Bundle ID, all matching processes will be signaled. + +Unlike `quit:` directives, Unix signals originate from the current user, not from the superuser. This is construed as a safety feature, since the superuser is capable of bringing down the system via signals. However, this inconsistency may also be considered a bug, and should be addressed in some fashion in a future version. + +## uninstall key login_item: + +Login items associated with an Application bundle on disk can be listed using the command: + +```bash +$ ./developer/bin/list_login_items_for_app +``` + +Note that you will likely need to have opened the app at least once for any login items to be present. + +## uninstall Key kext: + +IDs for currently loaded kernel extensions can be listed using the command: + +```bash +$ ./developer/bin/list_loaded_kext_ids +``` + +IDs inside a kext bundle you have located on disk can be listed using the command: + +```bash +$ ./developer/bin/list_id_in_kext +``` + +## uninstall Key delete: + +`delete:` should only be used as a last resort, if other `uninstall` methods are insufficient. + +Arguments to `uninstall delete:` should be static, single-quoted, absolute paths. + +* Only single quotes should be used. +* Double-quotes should not be used. `ENV['HOME']` and other variables + should not be interpolated in the value. +* Basic tilde expansion is performed on paths, i.e., leading `~` is expanded to the home directory. +* Only absolute paths should be given. +* No glob expansion is performed (*eg* `*` characters are literal), though glob expansion is a desired future feature. + +To remove user-specific files, use the `zap` stanza. + +## uninstall Key trash: + +*stub* - currently a synonym for `delete:`. In the future this will cause files to be moved to the Trash. It is best not to use this stub until it gains the proper functionality. + +## Working With a pkg File Manually + +Advanced users may wish to work with a `pkg` file manually, without having the package installed. + +A list of files which may be installed from a `pkg` can be extracted using the command: + +```bash +$ ./developer/bin/list_payload_in_pkg +``` + +Candidate application names helpful for determining the name of a Cask may be extracted from a `pkg` file using the command: + +```bash +$ ./developer/bin/list_apps_in_pkg +``` + +Candidate package IDs which may be useful in a `pkgutil:` key may be extracted from a `pkg` file using the command: + +```bash +$ ./developer/bin/list_ids_in_pkg +``` + +A fully manual method for finding bundle ids in a package file follows: + +1. Unpack `/path/to/my.pkg` (replace with your package name) with `pkgutil --expand /path/to/my.pkg /tmp/expanded.unpkg`. +2. The unpacked package is a folder. Bundle ids are contained within files named `PackageInfo`. These files can be found with the command `find /tmp/expanded.unpkg -name PackageInfo`. +3. `PackageInfo` files are XML files, and bundle ids are found within the `identifier` attributes of `` tags that look like ``, where extraneous attributes have been snipped out and replaced with ellipses. +4. Kexts inside packages are also described in `PackageInfo` files. If any kernel extensions are present, the command `find /tmp/expanded.unpkg -name PackageInfo -print0 | xargs -0 grep -i kext` should return a `` tag with a `path` attribute that contains a `.kext` extension, for example ``. +5. Once bundle ids have been identified, the unpacked package directory can be deleted. diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/url.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/url.md new file mode 100644 index 0000000000..16034ee25a --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/url.md @@ -0,0 +1,132 @@ +# url + +## HTTPS URLs are Preferred + +If available, an HTTPS URL is preferred. A plain HTTP URL should only be used in the absence of a secure alternative. + +## Additional HTTP/S URL Parameters + +When a plain URL string is insufficient to fetch a file, additional information may be provided to the `curl`-based downloader, in the form of key/value pairs appended to `url`: + +| key | value | +| ------------------ | ----------- | +| `using:` | the symbol `:post` is the only legal value +| `cookies:` | a hash of cookies to be set in the download request +| `referer:` | a string holding the URL to set as referrer in the download request +| `user_agent:` | a string holding the user agent to set for the download request. Can also be set to the symbol `:fake`, which will use a generic Browser-like user agent string. We prefer `:fake` when the server does not require a specific user agent. +| `data:` | a hash of parameters to be set in the POST request + +Example of using `cookies:`: [java.rb](https://github.com/caskroom/homebrew-cask/blob/472930df191d66747a57d5c96c0d00511d56e21b/Casks/java.rb#L5#L8) + +Example of using `referer:`: [rrootage.rb](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/rrootage.rb#L5) + +## When URL and Homepage Hostnames Differ, Add a Comment + +When the hostnames of `url` and `homepage` differ, the discrepancy should be documented with a comment of the form: + +``` +# URL_SECTION was verified as official when first introduced to the cask +``` + +Where `URL_SECTION` is the smallest possible portion of the URL that uniquely identifies the app or vendor. Examples can be seen in [`airfoil.rb`](https://github.com/caskroom/homebrew-cask/blob/1666993ee93e2a43f00a4dfc3c727da7c0b5ada9/Casks/airfoil.rb#L5), [`knockknock.rb`](https://github.com/caskroom/homebrew-cask/blob/6645a6090d1cb8fc026f243a47048749b31c32bf/Casks/knockknock.rb#L5), [`lightpaper.rb`](https://github.com/caskroom/homebrew-cask/blob/7a75f4e84c01bf192bd55f251b96cf2c1e086281/Casks/lightpaper.rb#L5), [`airtool.rb`](https://github.com/caskroom/homebrew-cask/blob/355211a8a3ea54046ae45022bcf71980bd2d5432/Casks/airtool.rb#L5), [`screencat.rb`](https://github.com/caskroom/homebrew-cask/blob/5fc818752c30c156c00f79b04b66406189ab2f30/Casks/screencat.rb#L5), [`0ad.rb`](https://github.com/caskroom/homebrew-cask/blob/7a75f4e84c01bf192bd55f251b96cf2c1e086281/Casks/0ad.rb#L5). + +These comments must be added so a user auditing the cask knows the URL was verified by the Homebrew-Cask team as the one provided by the vendor, even though it may look unofficial or suspicious. It is our responsibility as Homebrew-Cask maintainers to verify both the `url` and `homepage` information when first added (or subsequently modified, apart from versioning). + +The comment doesn’t mean you should trust the source blindly, but we only approve casks in which users can easily verify its authenticity with basic means, such as checking the official homepage or public repository. occasionally, slightly more elaborate techniques may be used, such as inspecting an [`appcast`](appcast.md) we established as official. Cases where such quick verifications aren’t possible (e.g. when the download URL is behind a registration wall) are [treated in a stricter manner](../../development/adding-a-cask.md#unofficial-vendorless-and-walled-builds). + +## Difficulty Finding a URL + +Web browsers may obscure the direct `url` download location for a variety of reasons. Homebrew-Cask supplies a script which can read extended file attributes to extract the actual source URL for most files downloaded by a browser on macOS. The script usually emits multiple candidate URLs; you may have to test each of them: + +```bash +$ $(brew --repository)/Library/Taps/caskroom/homebrew-cask/developer/bin/list_url_attributes_on_file +``` + +## Subversion URLs + +In rare cases, a distribution may not be available over ordinary HTTP/S. Subversion URLs are also supported, and can be specified by appending the following key/value pairs to `url`: + +| key | value | +| ------------------ | ----------- | +| `using:` | the symbol `:svn` is the only legal value +| `revision:` | a string identifying the subversion revision to download +| `trust_cert:` | set to `true` to automatically trust the certificate presented by the server (avoiding an interactive prompt) + +## SourceForge/OSDN URLs + +SourceForge and OSDN (formerly SourceForge.JP) projects are common ways to distribute binaries, but they provide many different styles of URLs to get to the goods. + +We prefer URLs of this format: + +``` +https://downloads.sourceforge.net/{{project_name}}/{{filename}}.{{ext}} +``` + +Or, if it’s from [OSDN](https://osdn.jp/): + +``` +http://{{subdomain}}.osdn.jp/{{project_name}}/{{release_id}}/{{filename}}.{{ext}} +``` + +`{{subdomain}}` is typically of the form `dl` or `{{user}}.dl`. + +If these formats are not available, and the application is macOS-exclusive (otherwise a command-line download defaults to the Windows version) we prefer the use of this format: + +``` +https://sourceforge.net/projects/{{project_name}}/files/latest/download +``` + +## Personal Hosting Such as Dropbox + +URLs from dropbox.com or cl.ly/cloudapp.com are not readily distinguishable as being controlled by the original software vendor. These URLs should be used only when given as such on the official project website. + +Also make sure to give the URL for the binary download itself, rather than a preview page. (See .) + +## Some Providers Block Command-line Downloads + +Some hosting providers actively block command-line HTTP clients. Such URLs cannot be used in Casks. + +Some providers do not actively block command-line HTTP clients but use URLs that change periodically, or even on each visit (example: FossHub). For those, see section [URLs that Change on Every Visit](#urls-that-change-on-every-visit). + +## Vendor URLs Are Preferred + +When possible, it is best to use a download URL from the original developer or vendor, rather than an aggregator such as `macupdate.com`. + +## URLs that Change on Every Visit + +Some providers use disposable URLs, which a Cask author cannot know in advance. Such URLs may change daily, or on every visit, and sometimes need to be dynamically obtained from a landing site. + +### The Problem + +In theory, one can write arbitrary Ruby code right in the Cask definition to fetch and construct a disposable URL. + +However, this typically involves an HTTP/S round trip to a landing site, which may take a long time. Because of the way Homebrew-Cask loads and parses Casks, it is not acceptable that such expensive operations be performed directly in the body of a Cask definition. + +### Using a Block to Defer Code Execution + +Similar to the `preflight`, `postflight`, `uninstall_preflight`, and `uninstall_postflight` blocks, the `url` stanza offers an optional _block syntax_: + +```rb +url do + # No known stable URL; fetching disposable URL from landing site + open('https://example.com/app/landing') do |landing_page| + content = landing_page.read + parse(content) # => https://example.com/download?23309800482283 + end +end +``` + +The block is only evaluated when needed, for example on download time or when auditing a Cask. +Inside a block, you may safely do things such as HTTP/S requests that may take a long time to execute. You may also refer to the `@cask` instance variable, and invoke any method available on `@cask`. + +The block will be called immediately before downloading; its result value will be assumed to be a `String` and subsequently used as a download URL. + +You can use the `url` stanza with either a direct argument or a block but not with both. + +Example for using the block syntax: [audacity.rb](https://github.com/caskroom/homebrew-cask/blob/c389d9ccbb46d30b6ac1cbdbadf49591ca8ff6cd/Casks/audacity.rb#L5-L15) + +### Mixing Additional URL Parameters With the Block Syntax + +In rare cases, you might need to set URL parameters like `cookies` or `referer` while also using the block syntax. + +This is possible by returning a two-element array as a block result. The first element of the array must be the download URL; the second element must be a `Hash` containing the parameters. diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/version.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/version.md new file mode 100644 index 0000000000..923b8593b2 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/version.md @@ -0,0 +1,53 @@ +# version + +`version`, while related to the app’s own versioning, doesn’t have to follow it exactly. It is common to change it slightly so it can be [interpolated](https://en.wikipedia.org/wiki/String_interpolation#Ruby) in other stanzas, usually in `url` to create a Cask that only needs `version` and `sha256` changes when updated. This can be taken further, when needed, with [ruby String methods](https://ruby-doc.org/core/String.html). + +For example: + +Instead of + +```ruby +version '1.2.3' +url 'http://example.com/file-version-123.dmg' +``` + +We can use + +```ruby +version '1.2.3' +url "http://example.com/file-version-#{version.delete('.')}.dmg" +``` + +We can also leverage the power of regular expressions. So instead of + +```ruby +version '1.2.3build4' +url 'http://example.com/1.2.3/file-version-1.2.3build4.dmg' +``` + +We can use + +```ruby +version '1.2.3build4' +url "http://example.com/#{version.sub(%r{build\d+}, '')}/file-version-#{version}.dmg" +``` + +## version methods + +The examples above can become hard to read, however. Since many of these changes are common, we provide a number of helpers to clearly interpret otherwise obtuse cases: + +| Method | Input | Output | +|--------------------------|--------------------|--------------------| +| `major` | `1.2.3-a45,ccdd88` | `1` | +| `minor` | `1.2.3-a45,ccdd88` | `2` | +| `patch` | `1.2.3-a45,ccdd88` | `3` | +| `major_minor` | `1.2.3-a45,ccdd88` | `1.2` | +| `major_minor_patch` | `1.2.3-a45,ccdd88` | `1.2.3` | +| `before_comma` | `1.2.3-a45,ccdd88` | `1.2.3-a45` | +| `after_comma` | `1.2.3-a45,ccdd88` | `ccdd88` | +| `dots_to_hyphens` | `1.2.3-a45,ccdd88` | `1-2-3-a45,ccdd88` | +| `no_dots` | `1.2.3-a45,ccdd88` | `123-a45,ccdd88` | + +Similar to `dots_to_hyphens`, we provide all logical permutations of `{dots,hyphens,underscores,slashes}_to_{dots,hyphens,underscores,slashes}`. The same applies to `no_dots` in the form of `no_{dots,hyphens,underscores,slashes}`, with an extra `no_dividers` that applies all of those at once. + +Finally, there are `before_colon` and `after_colon` that act like their `comma` counterparts. These four are extra special to allow for otherwise complex cases, and should be used sparingly. There should be no more than one of `,` and `:` per `version`. Use `,` first, and `:` only if absolutely necessary. diff --git a/Library/Homebrew/cask/doc/cask_language_reference/stanzas/zap.md b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/zap.md new file mode 100644 index 0000000000..f06699b27a --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/stanzas/zap.md @@ -0,0 +1,24 @@ +# zap + +## zap Stanza Purpose + +The `zap` stanza describes a more complete uninstallation of files associated with a Cask. The `zap` procedures will never be performed by default, but only if the user invokes the `zap` verb: + +```bash +$ brew cask zap td-toolbelt # also removes org.ruby-lang.installer +``` + +`zap` stanzas may remove: + +* Preference files and caches stored within the user’s `~/Library` directory. +* Shared resources such as application updaters. Since shared resources may be removed, other applications may be affected by `brew cask zap`. Understanding that is the responsibility of the end user. + +`zap` stanzas should not remove: + +* Files created by the user directly. + +## zap Stanza Syntax + +The form of `zap` stanza follows the [`uninstall` stanza](uninstall.md). All of the same directives are available. Unlike with `uninstall`, however, `delete:` is not discouraged in `zap`. + +Example: [injection.rb](https://github.com/caskroom/homebrew-cask/blob/312ae841f1f1b2ec07f4d88b7dfdd7fbdf8d4f94/Casks/injection.rb#L16) diff --git a/Library/Homebrew/cask/doc/cask_language_reference/token_reference.md b/Library/Homebrew/cask/doc/cask_language_reference/token_reference.md new file mode 100644 index 0000000000..4f28c9a6b3 --- /dev/null +++ b/Library/Homebrew/cask/doc/cask_language_reference/token_reference.md @@ -0,0 +1,127 @@ +# Cask Token Reference + +This document describes the algorithm implemented in the `generate_cask_token` script, and covers detailed rules and exceptions which are not needed in most cases. + +* [Purpose](#purpose) +* [Finding the Simplified Name of the Vendor’s Distribution](#finding-the-simplified-name-of-the-vendors-distribution) +* [Converting the Simplified Name To a Token](#converting-the-simplified-name-to-a-token) +* [Cask Filenames](#cask-filenames) +* [Cask Headers](#cask-headers) +* [Cask Token Examples](#cask-token-examples) +* [Token Overlap](#token-overlap) + +## Purpose + +The purpose of these stringent conventions is to: + +* Unambiguously boil down the name of the software into a unique identifier +* Minimize renaming events +* Prevent duplicate submissions + +The token itself should be: + +* Suitable for use as a filename +* Mnemonic + +Details of software names and brands will inevitably be lost in the conversion to a minimal token. To capture the vendor’s full name for a distribution, use the [`name`](https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/name.md) within a Cask. `name` accepts an unrestricted UTF-8 string. + +## Finding the Simplified Name of the Vendor’s Distribution + +### Simplified Names of Apps + +* Start with the exact name of the Application bundle as it appears on disk, such as `Google Chrome.app`. + +* If the name uses letters outside A-Z, convert it to ASCII as described in [Converting to ASCII](#converting-to-ascii). + +* Remove `.app` from the end. + +* Remove from the end: the string “app”, if the vendor styles the name like “Software App.app”. Exception: when “app” is an inseparable part of the name, without which the name would be inherently nonsensical, as in [rcdefaultapp.rb](../../Casks/rcdefaultapp.rb). + +* Remove from the end: version numbers or incremental release designations such as “alpha”, “beta”, or “release candidate”. Strings which distinguish different capabilities or codebases such as “Community Edition” are currently accepted. Exception: when a number is not an incremental release counter, but a differentiator for a different product from a different vendor, as in [pgadmin3.rb](../../Casks/pgadmin3.rb). + +* If the version number is arranged to occur in the middle of the App name, it should also be removed. Example: [IntelliJ IDEA 13 CE.app](../../../../../homebrew-versions/tree/master/Casks/intellij-idea-ce.rb). + +* Remove from the end: “Launcher”, “Quick Launcher”. + +* Remove from the end: strings such as “Mac”, “for Mac”, “for OS X”, “macOS”, “for macOS”. These terms are generally added to ported software such as “MAME OS X.app”. Exception: when the software is not a port, and “Mac” is an inseparable part of the name, without which the name would be inherently nonsensical, as in [PlayOnMac.app](../../Casks/playonmac.rb). + +* Remove from the end: hardware designations such as “for x86”, “32-bit”, “ppc”. + +* Remove from the end: software framework names such as “Cocoa”, “Qt”, “Gtk”, “Wx”, “Java”, “Oracle JVM”, etc. Exception: the framework is the product being Casked, as in [java.rb](../../Casks/java.rb). + +* Remove from the end: localization strings such as “en-US”. + +* If the result of that process is a generic term, such as “Macintosh Installer”, try prepending the name of the vendor or developer, followed by a hyphen. If that doesn’t work, then just create the best name you can, based on the vendor’s web page. + +* If the result conflicts with the name of an existing Cask, make yours unique by prepending the name of the vendor or developer, followed by a hyphen. Example: [unison.rb](../../Casks/unison.rb) and [panic-unison.rb](../../Casks/panic-unison.rb). + +* Inevitably, there are a small number of exceptions not covered by the rules. Don’t hesitate to [contact the maintainers](../../../../issues) if you have a problem. + +### Converting to ASCII + +* If the vendor provides an English localization string, that is preferred. Here are the places it may be found, in order of preference: + + - `CFBundleDisplayName` in the main `Info.plist` file of the app bundle + - `CFBundleName` in the main `Info.plist` file of the app bundle + - `CFBundleDisplayName` in `InfoPlist.strings` of an `en.lproj` localization directory + - `CFBundleName` in `InfoPlist.strings` of an `en.lproj` localization directory + - `CFBundleDisplayName` in `InfoPlist.strings` of an `English.lproj` localization directory + - `CFBundleName` in `InfoPlist.strings` of an `English.lproj` localization directory + +* When there is no vendor localization string, romanize the name by transliteration or decomposition. + +* As a last resort, translate the name of the app bundle into English. + +### Simplified Names of `pkg`-based Installers + +* The Simplified Name of a `pkg` may be more tricky to determine than that of an App. If a `pkg` installs an App, then use that App name with the rules above. If not, just create the best name you can, based on the vendor’s web page. + +### Simplified Names of non-App Software + +* Currently, rules for generating a token are not well-defined for Preference Panes, QuickLook plugins, and several other types of software installable by Homebrew-Cask. Just create the best name you can, based on the filename on disk or the vendor’s web page. Watch out for duplicates. + + Non-app tokens should become more standardized in the future. + +## Converting the Simplified Name To a Token + +The token is the primary identifier for a package in our project. It’s the unique string users refer to when operating on the Cask. + +To convert the App’s Simplified Name (above) to a token: + +* Convert all letters to lower case. +* Expand the `+` symbol into a separated English word: `-plus-`. +* Expand the `@` symbol into a separated English word: `-at-`. +* Spaces become hyphens. +* Hyphens stay hyphens. +* Digits stay digits. +* Delete any character which is not alphanumeric or a hyphen. +* Collapse a series of multiple hyphens into one hyphen. +* Delete a leading or trailing hyphen. + +We avoid defining Cask tokens in the repository which differ only by the placement of hyphens. Prepend the vendor name if needed to disambiguate the token. + +## Cask Filenames + +Casks are stored in a Ruby file named after the token, with the file extension `.rb`. + +## Cask Headers + +The token is also given in the header line for each Cask. + +## Cask Token Examples + +These illustrate most of the rules for generating a token: + +App Name on Disk | Simplified App Name | Cask Token | Filename +-----------------------|---------------------|------------------|---------------------- +`Audio Hijack Pro.app` | Audio Hijack Pro | audio-hijack-pro | `audio-hijack-pro.rb` +`VLC.app` | VLC | vlc | `vlc.rb` +`BetterTouchTool.app` | BetterTouchTool | bettertouchtool | `bettertouchtool.rb` +`LPK25 Editor.app` | LPK25 Editor | lpk25-editor | `lpk25-editor.rb` +`Sublime Text 2.app` | Sublime Text | sublime-text | `sublime-text.rb` + +# Token Overlap + +When the token for a new Cask would otherwise conflict with the token of an already existing Cask, the nature of that overlap dictates the token (for possibly both Casks). See [Finding a Home For Your Cask](../development/adding_a_cask.md#finding-a-home-for-your-cask) for information on how to proceed. + +# <3 THANK YOU TO ALL CONTRIBUTORS! <3 diff --git a/Library/Homebrew/cask/doc/development/adding_a_cask.md b/Library/Homebrew/cask/doc/development/adding_a_cask.md new file mode 100644 index 0000000000..d69966a2a3 --- /dev/null +++ b/Library/Homebrew/cask/doc/development/adding_a_cask.md @@ -0,0 +1,314 @@ +## Adding a Cask + +Making a new Cask is easy. Follow the directions in [Getting Set Up To Contribute](../../CONTRIBUTING.md#getting-set-up-to-contribute) to begin. + +### Examples + +Here’s a Cask for `shuttle` as an example. Note the comment above `url`, which is needed when [the url and homepage hostnames differ](../cask_language_reference/stanzas/url.md#when-url-and-homepage-hostnames-differ-add-a-comment) + +```ruby +cask 'shuttle' do + version '1.2.6' + sha256 '7b54529cd00332e423839cf768b732ac6c42e17de9325d0a093764180deeb611' + + # github.com/fitztrev/shuttle was verified as official when first introduced to the cask + url "https://github.com/fitztrev/shuttle/releases/download/v#{version}/Shuttle.zip" + appcast 'https://github.com/fitztrev/shuttle/releases.atom', + checkpoint: 'c3dea2ed479b3ebba7c56ace6040901795f6dc6be92f9ffc30cc808d31723f17' + name 'Shuttle' + homepage 'https://fitztrev.github.io/shuttle/' + license :mit + + app 'Shuttle.app' + + zap delete: '~/.shuttle.json' +end +``` + +And here is one for `airstream`. Note that it has an unversioned download (the download `url` does not contain the version number, unlike the example above). It also suppresses the checksum with `sha256 :no_check` (necessary since the checksum will change when a new distribution is made available). This combination of `version :latest` and `sha256 :no_check` is currently the preferred mechanism when a versioned download URL is not available. + +```ruby +cask 'airstream' do + version :latest + sha256 :no_check + + # amazonaws.com/airstream-clients was verified as official when first introduced to the cask + url 'https://s3-us-west-2.amazonaws.com/airstream-clients/mac/airstream-mac.dmg' + name 'AirStream' + homepage 'http://airstream.io/download/' + license :gratis + + app 'AirStream.app' + + caveats do + depends_on_java('6') + end +end +``` + +Here is a last example for `airdisplay`, which uses a `pkg` installer to install the application instead of a stand-alone application bundle (`.app`). Note the [`uninstall pkgutil` stanza](../cask_language_reference/stanzas/uninstall.md#uninstall-key-pkgutil), which is needed to uninstall all files which were installed using the installer. + +```ruby +cask 'airdisplay' do + version '3.0.3' + sha256 'db84a66fe3522929a0afa58a4fe0189977baded89df0035ead1ccd334f7b8126' + + url "https://www.avatron.com/updates/software/airdisplay/ad#{version.no_dots}.zip" + appcast 'https://avatron.com/updates/software/airdisplay/appcast.xml', + checkpoint: '938bdb9fbee793dce92818366cb2c19ba84c5b0cd6853fd893897d4a40689bc2' + name 'Air Display' + homepage 'https://avatron.com/apps/air-display/' + license :commercial + + pkg 'Air Display Installer.pkg' + + uninstall pkgutil: 'com.avatron.pkg.AirDisplay' +end +``` + +### Generating a Token for the Cask + +The Cask **token** is the mnemonic string people will use to interact with the Cask via `brew cask install`, `brew cask search`, etc. The name of the Cask **file** is simply the token with the extension `.rb` appended. + +The easiest way to generate a token for a Cask is to run this command: + +```bash +$ "$(brew --repository)/Library/Taps/caskroom/homebrew-cask/developer/bin/generate_cask_token" '/full/path/to/new/software.app' +``` + +If the software you wish to Cask is not installed, or does not have an associated App bundle, just give the full proper name of the software instead of a pathname: + +```bash +$ "$(brew --repository)/Library/Taps/caskroom/homebrew-cask/developer/bin/generate_cask_token" 'Google Chrome' +``` + +If the `generate_cask_token` script does not work for you, see [Cask Token Details](#cask-token-details). + +### The `brew cask create` Command + +Once you know the token, create your Cask with the handy-dandy `brew cask create` command: + +```bash +$ brew cask create my-new-cask +``` + +This will open `$EDITOR` with a template for your new Cask, to be stored in the file `my-new-cask.rb`. Running the `create` command above will get you a template that looks like this: + +```ruby +cask 'my-new-cask' do + version '' + sha256 '' + + url '' + name '' + homepage '' + license :unknown # TODO: change license and remove this comment; ':unknown' is a machine-generated placeholder + + app '' +end +``` + +### Cask Stanzas + +Fill in the following stanzas for your Cask: + +| name | value | +| ------------------ | ----------- | +| `version` | application version; give the value `:latest` if only an unversioned download is available +| `sha256` | SHA-256 checksum of the file downloaded from `url`, calculated by the command `shasum -a 256 `. Can be suppressed by using the special value `:no_check`. (see [sha256](../cask_language_reference/stanzas/sha256.md)) +| `url` | URL to the `.dmg`/`.zip`/`.tgz`/`.tbz2` file that contains the application.
A [comment](../cask_language_reference/stanzas/url.md#when-url-and-homepage-hostnames-differ-add-a-comment) should be added if the hostnames in the `url` and `homepage` stanzas differ. Block syntax should be used for URLs that change on every visit.
See [URL Stanza Details](../cask_language_reference/stanzas/url.md) for more information. +| `name` | the full and proper name defined by the vendor, and any useful alternate names (see [Name Stanza Details](../cask_language_reference/stanzas/name.md)) +| `homepage` | application homepage; used for the `brew cask home` command +| `license` | a symbol identifying the license for the application. Valid category licenses include `:oss`, `:closed`, and `:unknown`. It is OK to leave as `:unknown`. (see [License Stanza Details](../cask_language_reference/stanzas/license.md)) +| `app` | relative path to an `.app` bundle that should be moved into the `/Applications` folder on installation (see [App Stanza Details](../cask_language_reference/stanzas/app.md)) + +Other commonly-used stanzas are: + +| name | value | +| ------------------ | ----------- | +| `appcast` | a URL providing an appcast feed to find updates for this Cask. (see [Appcast Stanza Details](../cask_language_reference/stanzas/appcast.md)) +| `pkg` | relative path to a `.pkg` file containing the distribution (see [Pkg Stanza Details](../cask_language_reference/stanzas/pkg.md)) +| `caveats` | a string or Ruby block providing the user with Cask-specific information at install time (see [Caveats Stanza Details](../cask_language_reference/stanzas/caveats.md)) +| `uninstall` | procedures to uninstall a Cask. Optional unless the `pkg` stanza is used. (see [Uninstall Stanza Details](../cask_language_reference/stanzas/uninstall.md)) + +Additional `artifact` stanzas you might need for special use-cases can be found [here](../cask_language_reference/all_stanzas.md#at-least-one-artifact-stanza-is-also-required). Even more special-use stanzas are listed at [Optional Stanzas](../cask_language_reference/all_stanzas.md#optional-stanzas). + +### Cask Token Details + +If a token conflicts with an already-existing Cask, authors should manually make the new token unique by prepending the vendor name. Example: [unison.rb](../../Casks/unison.rb) and [panic-unison.rb](../../Casks/panic-unison.rb). + +If possible, avoid creating tokens which differ only by the placement of hyphens. + +To generate a token manually, or to learn about exceptions for unusual cases, see [token_reference.md](../cask_language_reference/token_reference.md). + +### Archives With Subfolders + +When a downloaded archive expands to a subfolder, the subfolder name must be included in the `app` value. + +Example: + +1. Texmaker is downloaded to the file `TexmakerMacosxLion.zip`. +2. `TexmakerMacosxLion.zip` unzips to a folder called `TexmakerMacosxLion`. +3. The folder `TexmakerMacosxLion` contains the application `texmaker.app`. +4. So, the `app` stanza should include the subfolder as a relative path: + + ```ruby + app 'TexmakerMacosxLion/texmaker.app' + ``` + + +## Testing Your New Cask + +Give it a shot with `brew cask install my-new-cask`. + +Did it install? If something went wrong, `brew cask uninstall my-new-cask` and edit your Cask with `brew cask edit my-new-cask` to fix it. + +If everything looks good, you’ll also want to make sure your Cask passes audit with: + +```bash +brew cask audit my-new-cask --download +``` + +You should also check stylistic details with `brew cask style`: + +```bash +$ cd "$(brew --repository)"/Library/Taps/caskroom/homebrew-cask +$ brew cask style Casks/my-new-cask.rb [--fix] +``` + +Keep in mind all of these checks will be made when you submit your PR, so by doing them in advance you’re saving everyone a lot of time and trouble. + +If your application and Homebrew-Cask do not work well together, feel free to [file an issue](https://github.com/caskroom/homebrew-cask#reporting-bugs) after checking out open issues. + +## Finding a Home For Your Cask + +We maintain separate Taps for different types of binaries. Our nomenclature is: + ++ **Stable**: The latest version provided by the developer defined by them as such. ++ **Beta, Development, Unstable**: Subsequent versions to **stable**, yet incomplete and under development, aiming to eventually become the new **stable**. ++ **Nightly**: Constantly up-to-date versions of the current development state. ++ **Legacy**: Any **stable** version that is not the most recent. ++ **Alternative**: Alternative edition of an existing app, by the same vendor (developer editions, community editions, pro editions, …). ++ **Regional, Localized**: Any version that isn’t the US English one, when that exists. ++ **Trial**: Date-limited version that stops working entirely after it expires, requiring payment to lift the limitation. ++ **Freemium**: Gratis version that works indefinitely but with limitations that can be removed by paying. ++ **Fork**: An alternate version of an existing project, with a based-on but modified source and binary. ++ **Unofficial**: An *allegedly* unmodified compiled binary, by a third-party, of a binary that has no existing build by the owner of the source code. ++ **Vendorless**: A binary distributed without an official website, like a forum posting. ++ **Walled**: When the download URL is both behind a login/registration form and from a host that differs from the homepage. + +### Stable Versions + +Stable versions live in the main repository at [caskroom/homebrew-cask](https://github.com/caskroom/homebrew-cask). They should run on the latest release of macOS or the previous point release (in 2015, for example, that meant El Capitan and Yosemite). + +### But There Is No Stable Version! + +When an App is only available as beta, development, or unstable versions, or in cases where such a version is the general standard, then said version can go into the main repo. + +### Beta, Unstable, Development, Nightly, Legacy, or Alternative Versions + +When an App has a principal stable version, alternative versions should be submitted to [caskroom/homebrew-versions](https://github.com/caskroom/homebrew-versions). + +### Regional and Localized + +When an App exists in more than one language or has different regional editions, the US English one belongs in the main repo, and all the others in [caskroom/homebrew-versions](https://github.com/caskroom/homebrew-versions). When not already part of the name of the App, a [regional identifier](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) and a [language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) are to be appended to the Cask’s token (both when available, or just the appropriate one when not). + +### Trial and Freemium Versions + +Before submitting a trial, make sure it can be made into a full working version without the need to be redownloaded. If an App provides a trial but the only way to buy the full version is via the Mac App Store, it does not belong in any of the official repos. Freemium versions are fine. + +### Forks and Apps with Conflicting Names + +Forks should have the vendor’s name as a prefix on the Cask’s file name and token. For unrelated Apps that share a name, the most popular one (usually the one already present) stays unprefixed. Since this can be subjective, if you disagree with a decision open an issue and make your case to the maintainers. + +### Unofficial, Vendorless, and Walled Builds + +We do not accept these casks since they offer a higher-than-normal security risk. [alehouse/homebrew-unofficial](https://github.com/alehouse/homebrew-unofficial) is a sister repo where you may wish to submit your cask. + +### Fonts + +Font Casks live in the [caskroom/homebrew-fonts](https://github.com/caskroom/homebrew-fonts) repository. See the font repo [CONTRIBUTING.md](../../../../../homebrew-fonts/blob/master/CONTRIBUTING.md) +for details. + +## Submitting Your Changes + +Hop into your Tap and check to make sure your new Cask is there: + +```bash +$ cd "$(brew --repository)"/Library/Taps/caskroom/homebrew-cask +$ git status +# On branch master +# Untracked files: +# (use "git add ..." to include in what will be committed) +# +# Casks/my-new-cask.rb +``` + +So far, so good. Now make a feature branch that you’ll use in your pull request: + +```bash +$ git checkout -b my-new-cask +Switched to a new branch 'my-new-cask' +``` + +Stage your Cask with `git add Casks/my-new-cask.rb`. You can view the changes that are to be committed with `git diff --cached`. + +Commit your changes with `git commit -v`. + +### Commit Messages + +For any git project, some good rules for commit messages are: + +* The first line is commit summary, 50 characters or less, +* Followed by an empty line, +* Followed by an explanation of the commit, wrapped to 72 characters. + +See [a note about git commit messages](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) for more. + +The first line of a commit message becomes the **title** of a pull request on GitHub, like the subject line of an email. Including the key info in the first line will help us respond faster to your pull. + +For Cask commits in the Homebrew-Cask project, we like to include the Application name, version number (or `:latest`), and purpose of the commit in the first line. + +Examples of good, clear commit summaries: + +* `Add Transmission.app v1.0` +* `Upgrade Transmission.app to v2.82` +* `Fix checksum in Transmission.app Cask` +* `Add CodeBox Latest` + +Examples of difficult, unclear commit summaries: + +* `Upgrade to v2.82` +* `Checksum was bad` + +### Pushing + +Push your changes to your GitHub account: + +```bash +$ github_user='' +$ git push "$github_user" my-new-cask +``` + +If you are using [GitHub two-factor authentication](https://help.github.com/articles/about-two-factor-authentication/) and set your remote repository as HTTPS you will need to set up a personal access token and use that instead of your password. Further information [here](https://help.github.com/articles/https-cloning-errors/#provide-access-token-if-2fa-enabled). + +### Squashing + +If your pull request has multiple commits which revise the same lines of code, or if you make some changes after comments from one of the maintainers, it is better to [squash](https://davidwalsh.name/squash-commits-git) those commits together into one logical unit. + +But you don’t always have to squash — it is fine for a pull request to contain multiple commits when there is a logical reason for the separation. + +### Filing a Pull Request on GitHub + +Now go to the [`homebrew-cask` GitHub repository](https://github.com/caskroom/homebrew-cask). GitHub will often show your `my-new-cask` branch with a handy button to `Compare & pull request`. Otherwise, click the `New pull request` button and choose to `compare across forks`. The base fork should be `caskroom/homebrew-cask @ master`, and the head fork should be `my-github-username/homebrew-cask @ my-new-cask`. You can also add any further comments to your pull request at this stage. + +Congratulations! You are done now, and your Cask should be pulled in or otherwise noticed in a while. If a maintainer suggests some changes, just make them on the `my-new-cask` branch locally, [squash](#squashing), and [push](#pushing). + +## Cleaning up + +After your Pull Request is submitted, you should get yourself back onto `master`, so that `brew update` will pull down new Casks properly: + +```bash +cd "$(brew --repository)"/Library/Taps/caskroom/homebrew-cask +git checkout master +``` diff --git a/Library/Homebrew/cask/doc/development/hacking.md b/Library/Homebrew/cask/doc/development/hacking.md new file mode 100644 index 0000000000..707ea1f716 --- /dev/null +++ b/Library/Homebrew/cask/doc/development/hacking.md @@ -0,0 +1,160 @@ +# Hacking on Homebrew-Cask + +If you’d like to hack on the Ruby code that drives this project, please join us, we’d love to have you! + +## Goals, Design, and Philosophy + +Homebrew-Cask is an attempt to make a Linux-style package manager for precompiled macOS software. Homebrew-Cask is not yet as featureful as `apt` or `yum`, but we are trying to be as close as we can get to those tools from the user’s point of view. + +We manage installed files via the “symlink farm” method, like [GNU Stow](https://www.gnu.org/software/stow/) and [Homebrew](http://brew.sh/). Similarly, we try to avoid `sudo` where possible. + +Homebrew-Cask is designed to work like a traditional Unix tool: + +* All functionality should be accessible from the CLI. The user should be freed (**freed!**) from interacting with a GUI. +* Homebrew-Cask should itself be scriptable. + +## Project Status + +Homebrew-Cask is still young, and should be considered in alpha. + +We have good support for a variety of artifacts: apps, pkgs, binaries, plugins, and [fonts](https://github.com/caskroom/homebrew-fonts/). Homebrew-Cask can install and uninstall any of those. However, these commands don’t work well with multiple versions, and most importantly, we currently can’t `upgrade`. + +Since upgrading is a core feature of every package manager, the implementation of an `upgrade` verb is our top priority. For `upgrade` to work reliably, we must: + +* Maintain unequivocal version information from a variety of sources, +* Track version-specific uninstallation, +* Play nice with self-updating software. + +These and more requirements are tracked in our [`upgrade` roadmap](https://github.com/caskroom/homebrew-cask/issues/4678). If you’d like to contribute to `upgrade`, that’s an excellent place to start. + +## Homebrew and Homebrew-Cask + +Homebrew-Cask is independent of Homebrew as a project. + +The Homebrew-Cask CLI is implemented as a Homebrew subcommand, so we try to match semantics wherever possible. That means that similar functionality should have similar flags and parameters. + +However, very little backend code is shared between the two projects. The Homebrew codebase is based on how Homebrew Formulae work, and our Casks are very different from Formulae. + +### Casks and Formulae + +Homebrew Formulae deal with many different build processes, and often include arbitrary Ruby code. + +Casks, by contrast, only need to support the few installation methods used by apps, pkg installers, and so on, making them suitable for a [declarative DSL](../cask_language_reference/). + +We encourage Cask authors to use the DSL as much as possible, since that makes things easier for everyone: from maintainers who review pull requests, to first-time contributors, to people who are unfamiliar with Ruby but would like to help. + +For software with unusual needs that are not covered by the DSL, we generally accept Casks containing small hacks or arbitrary code. If the hack becomes common enough, we extend the DSL with a simple shorthand that offers the same (or better) functionality. + +## Contributing + +### Setup + +Cask authors often work directly within the Homebrew directory under `/usr/local`. For coding, that is usually not sufficient. + +We recommend the following: + +1. Fork our repo: + +2. Clone a private copy of the repo: + + ```bash + git clone https://github.com//homebrew-cask.git + ``` + +3. Add the official repo as the `upstream` remote: + + ```bash + cd homebrew-cask + git remote add upstream https://github.com/caskroom/homebrew-cask.git + ``` + +4. Now you have two copies of the Homebrew-Cask codebase on disk: the released version in `/usr/local/Library/Taps/caskroom/homebrew-cask`, and a development version in your private repo. To symlink the `Casks` and `rubylib` folders from `/usr/local/...` into your private repo, run the following script: + + ```bash + /////developer/bin/develop_brew_cask + ``` + + Now you can hack on your private repo, and use the `brew cask` CLI like normal — it will interact with your latest code. + +5. Important: while in development mode, you can’t safely run Homebrew’s `brew update` command. To switch back to production mode, run: + + ```bash + /////developer/bin/production_brew_cask + ``` + +### Forcing a Ruby interpreter + +You can force a specific version of the Ruby interpreter, and/or an alternate version of the `brew-cask` subcommand, by invoking `brew cask` with fully-qualified paths, like this: + +```bash +$ /System/Library/Frameworks/Ruby.framework/Versions/Current/usr/bin/ruby /usr/local/Library/Taps/caskroom/homebrew-cask/cmd/brew-cask.rb help +``` + +### Forcing a Specific Homebrew-Cask Subcommand + +If you are developing a subcommand, you can force `brew cask` to dispatch a specific file by giving a fully-qualified path to the file containing the subcommand, like this: + +```bash +$ brew cask /usr/local/Library/Taps/caskroom/homebrew-cask/lib/hbc/cli/info.rb google-chrome +``` + +This form can also be combined with a specific Ruby interpreter as above. + +### Forcing a Specific macOS Release + +The environment variable `$MACOS_RELEASE` can be overridden at the command line for test purposes: + +```bash +$ MACOS_RELEASE=10.9 brew cask info +``` + +The environment variable `$MACOS_RELEASE_WITH_PATCHLEVEL` is also available, though not consulted directly. Use `$MACOS_RELEASE` for testing. + +### Target Ruby Versions + +Homebrew-Cask requires a Ruby interpreter version 2.0 or above. This is the default system Ruby on Mavericks (10.9) and later. + +### Submitting Your Changes + +See [the relevant section in `adding_a_cask.md`](adding_a_cask.md#submitting-your-changes). + +#### Commit Messages + +The first line of a commit message (the summary line) is like the subject line of an email. (See [`adding_a_cask.md`](adding_a_cask.md#commit-messages)). A short but complete summary line helps the maintainers respond to your pull request more quickly. + +#### Mind the test suite! + +If you’re making changes - please write some tests for them! Install dependencies and run the whole test suite with: + +```bash +brew cask-tests +``` + +Be sure to run the test suite before submitting. If you forget, Travis-CI will do that for you and embarrass you in front of all your friends. :) + +You may also use a set of environment variables to increase verbosity: + +* `TESTOPTS`, `TEST` etc. for the old [minitest suites](https://www.ruby-doc.org/stdlib-2.0.0/libdoc/rake/rdoc/Rake/TestTask.html) +* `SPEC_OPTS`, `SPEC` etc. for [rspec suites](http://apidock.com/rspec/Spec/Rake/SpecTask) +* `VERBOSE_TESTS` to see the standard output from the actual code = ignore the `shutup` helper + +Example of a very verbose output: + +```shell +TESTOPTS='-v' SPEC_OPTS='-fd' VERBOSE_TESTS=1 brew cask-tests +``` + +#### External Commands + +Advanced users may create their own external commands for Homebrew-Cask by following conventions similar to external commands for git or Homebrew. An external command may be any executable on your `$PATH` which follows the form `brewcask-`. (So long as `` does not conflict with an existing command verb.) The command will be invoked by `exec` and passed any unprocessed arguments from the original command-line. An external command may also be implemented as an executable Ruby file, on your `$PATH`, which follows the form `brewcask-.rb`. The Ruby file will be `required` and will have full access to the Ruby environments of both Homebrew-Cask and Homebrew. Example external commands may be found in `developer/examples`. + +## Hanging out on IRC + +We’re on IRC at `#homebrew-cask` on Freenode. If you are going to develop for Homebrew-Cask, it’s a great idea to hang out with us there. Here’s why: + +* Discuss your thoughts before coding and maybe get new ideas +* Get feedback from the Travis-CI bot on build failures +* Talk to [caskbot](https://github.com/passcod/caskbot) about checksums, version info, and releases +* Just to be social! + +# <3 THANK YOU! <3 diff --git a/Library/Homebrew/cask/doc/development/maintaining.md b/Library/Homebrew/cask/doc/development/maintaining.md new file mode 100644 index 0000000000..26b9abbb05 --- /dev/null +++ b/Library/Homebrew/cask/doc/development/maintaining.md @@ -0,0 +1,79 @@ +# Maintaining Homebrew-Cask + +__vv NOTE - DRAFT DOC! vv__ + +This doc is just at a starting point. The maintainers team will be collaborating on it and we’ll remove this header when we feel like it’s stable. + +__^^ NOTE - DRAFT DOC! ^^__ + +As a relatively large open source project with plenty of daily activity, Homebrew-Cask requires regular care and feeding. This includes reviewing and merging PRs, diagnosing bugs, improving documentation, discussing project policy and features, and plenty more! + +This responsibility is shared by @caskroom/maintainers - a team of humans spanning the globe each of whom has agreed to dedicate some of their spare time to helping our dear users. What a kind and friendly bunch they must be! (It’s true, they are.) + +As the project matures and grows, so does the team of maintainers. It’s becoming more and more important to write down things that once were done ad-hoc. + +So here is where we are gathering details about how we maintain the project. + +## Things we focus on + +* We favor the user above all. +* Any user that submits a PR to our little old project is solid gold - we do everything we can to make sure they have a good experience and that their work is appreciated. +* Friendliness. In our minds, we are in a *friendliness contest* against other open-source projects. We want to be the nicest, most fun, most easygoing project in the universe. +* Supporting each other. Help the other maintainers, and spread out the workload. + +## Reviewing Incoming Casks + +Casks are the lifeblood of this project, and they generate the most maintenance-requiring activity on the project. + +While we started as a tool for convenience, we’re working on adding safety and security to the list of things we do for our users. That means things like verifying download URLs, working to figure out file checksums when possible, *etc*. + +__TODO__: Maybe one of our more active Cask reviewers can fill in the things they look for in incoming Casks. + +## Labels + +Every open issue and pull request must have a label added to it, unless the maintainer immediately acts on it (closing/merging) after looking at it. Labels should be consistent across repositories: not every repository needs every label, but their meaning and color must be the same throughout. Currently, our labels are: + +Label | Description | Issues | Pull Requests +----- | ----------- | :----: | :-----------: +**bug** | Something isn’t working as expected. A modification/addition/removal. Must always be accompanied by **cask** or **core** | ✓ | ✓ +**cask** | Relates directly to a cask. Must always be accompanied by **bug** or **enhancement**. | ✓ | ✓ +**cask request** | Either a request for a new cask or a call for correction in an existing one. | ✓ | +**outdated appcast** | An automated label, handled by the various scripts geared towards updating casks with outdated appcasts. Should never be applied manually. | ✓ | ✓ +**chief bug** | When multiple people open new issues for the same bug, the main issue where its progression is being tracked should have this label. Every other one should be marked **duplicate** and closed. | ✓ | +**core** | Relates directly to the code of the core, Homebrew-Cask itself. Must always be accompanied by **bug** or **enhancement**. | ✓ | ✓ +**discussion** | A matter that benefits from discussion before a decision is to be made. Any opinion should be given by users and maintainers alike, even if that opinion is “I have no strong feelings on the matter”. | ✓ | +**documentation** | Relates to the documentation. | ✓ | ✓ +**duplicate** | An issue or pull request that is essentially the same as another. Should be immediately closed. | ✓ | ✓ +**enhancement** | Something we want implemented. Must always be accompanied by **cask** or **core**. | ✓ | ✓ +**future** | Something that can currently only be referenced and will only be possible to act upon in the future, after certain conditions are met. Currently references [changes to the installation behaviour](https://github.com/caskroom/homebrew-cask/issues/13201). To be used sparingly. | ✓ | ✓ +**meta** | Relates to Homebrew-Cask itself as a project and its policies/decisions. | ✓ | +**on hold** | A pull request that depends on another being merged before it itself can be as well. | | ✓ +**roadmap** | Roadmap for feature implementation. | ✓ | +**ready to implement** | Usually accompanied by the closing of a **discussion** issue. It succinctly describes in points the implementation of something yet to be written, be it a feature or a documentation section. Anyone looking at such an issue can safely ignore every post following the top one, as it should always be kept up-to-date with the discussion. | ✓ | +**travis** | Bug related to [Travis CI](https://travis-ci.org/). Must always be accompanied by **bug** or **enhancement**. | ✓ | ✓ +**upstream** | Something we have no hand in, and can only be fixed with intervention from developers outside Homebrew-Cask. Always refers to a cask, and never to the core. | ✓ | ✓ +**awaiting maintainer feedback** | A maintainer requires input from other maintainers to proceed. Other maintainers should occasionaly check this label and give their feedback on the subject, if able. | ✓ | ✓ +**awaiting user reply** | A maintainer requires further action or information from the original poster to proceed. Particularly useful to weed out those cases where issues and pull requests would otherwise be left open indefinitely because the original poster never replies. | ✓ | ✓ + +## Reviewing Core PRs + +Occasionally we’ll get submissions from users that fix bugs or add features to Homebrew-Cask itself. There is a subset of our maintainers who are less familiar with Ruby and prefer to leave these review to folks with more experience with the language. This is AOK! + +## Handling Cask Update PRs + +The most common pull requests we get are to add or update Casks. [Vítor Galvão](https://github.com/vitorgalvao) has created [some excellent scripts](https://github.com/vitorgalvao/tiny-scripts) to make these rote changes more painless. You can use [`fastmerge`](https://github.com/vitorgalvao/tiny-scripts/blob/master/fastmerge) if the PR is ready to merge (everything looks fine, all tests passed). Sometimes, new contributors aren't aware of how to squash commits, posting something like “Please [squash your commits](https://davidwalsh.name/squash-commits-git). Thanks!” should help them out. + +If the PR has an error, you can use [`prfixmaster`](https://github.com/vitorgalvao/tiny-scripts/blob/master/prfixmaster) to make any necessary changes. + +Lastly, if you see an outdated cask that just needs a version bump, you can use [`cask-repair`](https://github.com/vitorgalvao/tiny-scripts/blob/master/cask-repair) to make the PR yourself quickly. + +## Tips + +* To keep your repository up to date with caskroom/master, you can create a custom bash function to save some typing. Feel free to adapt the following set of commands to your specific needs --> `cd "$(brew --repository)"/Library/Taps/caskroom/homebrew-cask; git checkout master; git pull origin; git push "$GITHUB_USERNAME" master; git remote prune origin; git fetch -p origin; git remote update --prune` + +## Ideas for other things to include here + +* Productivity enhancing tips / tools / scripts that help with PR review, cask testing, etc. +* General policies. +* Documenting important decisions that have been made. +* Maybe some philosophical points about the project. diff --git a/Library/Homebrew/cask/doc/faq/apps_with_malware.md b/Library/Homebrew/cask/doc/faq/apps_with_malware.md new file mode 100644 index 0000000000..825c2af145 --- /dev/null +++ b/Library/Homebrew/cask/doc/faq/apps_with_malware.md @@ -0,0 +1,13 @@ +# Apps that bundle malware + +Unfortunately, in the world of software there are bad actors that bundle malware with their apps. Even so, Homebrew-Cask has long decided it is not a gatekeeper ([macOS already has one](https://support.apple.com/en-us/HT202491)) and [users are expected to know about the software they are installing](not_a_discoverability_service.md). This means we will not remove casks that link to these apps. We have several reasons for this, summarised in [a comment on issue #21399](https://github.com/caskroom/homebrew-cask/issues/21399#issuecomment-223148829). + +Within that context, we would still like for users to enjoy some kind of protection while minimising occurrences of legitimate developers being branded as malware carriers. We feel removing casks is an ineffective band-aid and the issue needs to be tackled earlier in the chain: at the macOS level. + +If an app that bundles malware was not signed with an Apple Developer ID and you purposefully disabled or bypassed Gatekeeper, no action will be taken on our part. When you disable security features, you do so at your own risk. If, however, an app that bundles malware is signed, Apple can revoke its permissions and it will no longer run on the computers of users that keep security features on — we all benefit, Homebrew-Cask users or not. **Note that for the time being, Homebrew-Cask will not quarantine download files. See [issue #22388](https://github.com/caskroom/homebrew-cask/issues/22388)**. + +To report a signed app that bundles malware, follow these steps: + +* Go to [Apple’s Bug Reporter](https://bugreport.apple.com/) and report the app that bundles malware. Be as precise as possible about how you know it bundles malware, and what steps reproduce your conclusions. Be sure to include relevant URLs, such as the app’s homepage. +* Make the report public at [Open Radar](http://www.openradar.me/). +* Submit a pull request with a [`malware` `caveat`](https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/caveats.md#caveats-mini-dsl). diff --git a/Library/Homebrew/cask/doc/faq/not_a_discoverability_service.md b/Library/Homebrew/cask/doc/faq/not_a_discoverability_service.md new file mode 100644 index 0000000000..022a02d5e8 --- /dev/null +++ b/Library/Homebrew/cask/doc/faq/not_a_discoverability_service.md @@ -0,0 +1,15 @@ +# Homebrew-Cask is not a discoverability service + +Ever since the inception of Homebrew-Cask, various requests fell under the umbrella of this reply. Though a somewhat popular request, after careful consideration on multiple occasions we’ve always come back to the same conclusion: we’re not a discoverability service and our users are expected to have reasonable knowledge about the apps they’re installing through us before doing so. + +Examples of requests under this same theme: + ++ [Separate by categories](https://github.com/caskroom/homebrew-cask/issues/5425). ++ [Rank by popularity](https://github.com/caskroom/homebrew-cask/issues/4323). ++ [Add descriptions](https://github.com/caskroom/homebrew-cask/issues/16089). + +Amongst other things, the logistics of such requests are unsustainable for Homebrew-Cask. Before making a request of this nature, you must read through those issues, as well as any other issues they link to, to get a full understanding of why that is the case, and why “but project *x* does *y*” arguments aren’t applicable, and not every package manager is the same. + +You should also be able to present clear actionable fixes to those concerns. Simply asking for it without solutions will get your issue closed. + +There is a difference between discoverability and searchability, however, and while the former (finding new apps you didn’t know about) is unlikely to ever become part of our goals, the later (identifying the app you know about and want to install) is indeed important to us, and we continue to work on it. diff --git a/Library/Homebrew/cask/doc/faq/rejected_casks.md b/Library/Homebrew/cask/doc/faq/rejected_casks.md new file mode 100644 index 0000000000..e62a286c1d --- /dev/null +++ b/Library/Homebrew/cask/doc/faq/rejected_casks.md @@ -0,0 +1,19 @@ +# Rejected Casks + +Before submitting a Cask to any of our repos, you must read [our documentation on acceptable Casks](../development/adding_a_cask.md#finding-a-home-for-your-cask) and perform a (at least quick) search to see if there were any previous attempts to introduce it. + +Common reasons to reject a Cask entirely: + ++ We have strong reasons to believe including the Cask can put the whole project at risk. Happened only once so far, [with Popcorn Time](https://github.com/caskroom/homebrew-cask/pull/3954). ++ The app is a trial version, and the only way to acquire the full version is through the Mac App Store ([documented](../development/adding_a_cask.md#trial-and-freemium-versions)). + + Similarly (and trickier to spot), the app has moved to the Mac App Store but still provides old versions via direct download. We reject these in all official repos so users don’t get stuck using an old version, wrongly thinking they’re using the most up-to-date one (which, amongst other things, might be a security risk). ++ The app is both open-source and CLI-only (i.e. it only uses the `binary` artifact). In that case, and [in the spirit of deduplication](https://github.com/caskroom/homebrew-cask/issues/15603), submit it first to [Homebrew](https://github.com/Homebrew/homebrew). If it is rejected there, you may then try again in Homebrew-Cask (link us to the issue on Homebrew so we can see their reasoning for rejection). ++ The app is open-source and has a GUI but no compiled versions (or only old ones) are provided. It’s better to have them in [Homebrew](https://github.com/Homebrew/homebrew) so users don’t get perpetually outdated versions. Examples include [`mpv`](https://github.com/caskroom/homebrew-cask/pull/20483) and [`gedit`](https://github.com/caskroom/homebrew-cask/pull/23360). ++ The app has been rejected before due to an issue we cannot fix, and this new submission doesn’t fix that . An example would be [the first submission of `soapui`](https://github.com/caskroom/homebrew-cask/pull/4939), whose installation problems were not fixed in the two subsequent submissions ([#9969](https://github.com/caskroom/homebrew-cask/pull/9969), [#10606](https://github.com/caskroom/homebrew-cask/pull/10606)). ++ The Cask is a duplicate. These submissions mostly occur when the [token reference](../cask_language_reference/token_reference.md) was not followed. ++ The download URL for the app is both behind a login/registration form and from a host that differs from the homepage, meaning users can’t easily verify its authenticity. [alehouse/homebrew-unofficial](https://github.com/alehouse/homebrew-unofficial) is a sister repo where you may wish to submit your cask. ++ The author has [specifically asked us not to include it](https://github.com/caskroom/homebrew-cask/pull/5342). + +Common reasons to reject a Cask from the main repo: + ++ The app is an alternate edition or a legacy or development version of a Cask already in the main repo, and as such should be submitted to [caskroom/versions](https://github.com/caskroom/homebrew-versions) ([documented](../development/adding_a_cask.md#beta-unstable-development-nightly-legacy-or-alternative-versions)). Common offenders include [Sublime Text 3](https://github.com/caskroom/homebrew-cask/search?utf8=%E2%9C%93&q=sublime+text+3&type=Issues) and [Firefox Developer Edition](https://github.com/caskroom/homebrew-cask/search?q=firefox+developer+edition&type=Issues&utf8=%E2%9C%93). diff --git a/Library/Homebrew/cask/doc/issue_templates/bug_report.md b/Library/Homebrew/cask/doc/issue_templates/bug_report.md new file mode 100644 index 0000000000..5726f466ac --- /dev/null +++ b/Library/Homebrew/cask/doc/issue_templates/bug_report.md @@ -0,0 +1,27 @@ +Bug report: +Remember to follow the [pre bug report](https://github.com/caskroom/homebrew-cask/blob/master/doc/reporting_bugs/pre_bug_report.md) guide beforehand. Failure to do so might get your issue closed. + +#### Description of issue + +[insert a detailed description of your issue here] + +
Output of `brew cask --verbose` + +``` +[paste output here] +``` +
+ +
Output of `brew doctor` + +``` +[paste output here] +``` +
+ +
Output of `brew cask doctor` + +``` +[paste output here] +``` +
diff --git a/Library/Homebrew/cask/doc/issue_templates/cask_request.md b/Library/Homebrew/cask/doc/issue_templates/cask_request.md new file mode 100644 index 0000000000..e0bc5ee343 --- /dev/null +++ b/Library/Homebrew/cask/doc/issue_templates/cask_request.md @@ -0,0 +1,14 @@ +Cask request: +### Cask details + +(Please fill out as much as possible) + +**Name** - + +**Homepage** - + +**License** - + +**Download URL** - + +**Description** - diff --git a/Library/Homebrew/cask/doc/issue_templates/feature_request.md b/Library/Homebrew/cask/doc/issue_templates/feature_request.md new file mode 100644 index 0000000000..ed78b1a8af --- /dev/null +++ b/Library/Homebrew/cask/doc/issue_templates/feature_request.md @@ -0,0 +1,13 @@ +Feature request: +### Description of feature/enhancement + + + +### Justification + + + +### Example use case + + + diff --git a/Library/Homebrew/cask/doc/issue_templates/issue_dead_app.md b/Library/Homebrew/cask/doc/issue_templates/issue_dead_app.md new file mode 100644 index 0000000000..6b7019a480 --- /dev/null +++ b/Library/Homebrew/cask/doc/issue_templates/issue_dead_app.md @@ -0,0 +1,8 @@ +Dead app: + +* Insert the name of the cask in the title, after the `:`. +* Insert the name of the cask and a link to it in the body of this issue (example: [`alfred`](https://github.com/caskroom/homebrew-cask/blob/master/Casks/alfred.rb)). +* Insert a link to the page that informs about the app being dead. +* After all that **delete all this pre-inserted template text**. + +Failure to follow these instructions may get your issue closed without further explanation. Thank you for taking the time to correctly report the issue. diff --git a/Library/Homebrew/cask/doc/issue_templates/issue_outdated_cask.md b/Library/Homebrew/cask/doc/issue_templates/issue_outdated_cask.md new file mode 100644 index 0000000000..4042f51f40 --- /dev/null +++ b/Library/Homebrew/cask/doc/issue_templates/issue_outdated_cask.md @@ -0,0 +1,8 @@ +Outdated cask: + +* Insert the name of the cask in the title, after the `:`. +* Insert the name of the cask and a link to it in the body of this issue (example: [`alfred`](https://github.com/caskroom/homebrew-cask/blob/master/Casks/alfred.rb)). +* Insert the new version of the app. +* After all that **delete all this pre-inserted template text**. + +Failure to follow these instructions may get your issue closed without further explanation. Thank you for taking the time to correctly report the issue. diff --git a/Library/Homebrew/cask/doc/issue_templates/issue_outdated_cask_but_cannot_find_link.md b/Library/Homebrew/cask/doc/issue_templates/issue_outdated_cask_but_cannot_find_link.md new file mode 100644 index 0000000000..6d27582a57 --- /dev/null +++ b/Library/Homebrew/cask/doc/issue_templates/issue_outdated_cask_but_cannot_find_link.md @@ -0,0 +1,8 @@ +Outdated cask but cannot find link: + +* Insert the name of the cask in the title, after the `:`. +* Insert the name of the cask and a link to it in the body of this issue (example: [`alfred`](https://github.com/caskroom/homebrew-cask/blob/master/Casks/alfred.rb)). +* Insert a detailed explanation of what you tried to do and why you failed to find the download link. +* After all that **delete all this pre-inserted template text**. + +Failure to follow these instructions may get your issue closed without further explanation. Thank you for taking the time to correctly report the issue. diff --git a/Library/Homebrew/cask/doc/issue_templates/issue_source_not_there_and_cannot_find_it.md b/Library/Homebrew/cask/doc/issue_templates/issue_source_not_there_and_cannot_find_it.md new file mode 100644 index 0000000000..053d86814f --- /dev/null +++ b/Library/Homebrew/cask/doc/issue_templates/issue_source_not_there_and_cannot_find_it.md @@ -0,0 +1,8 @@ +Source not there and cannot find it: + +* Insert the name of the cask in the title, after the `:`. +* Insert the name of the cask and a link to it in the body of this issue (example: [`alfred`](https://github.com/caskroom/homebrew-cask/blob/master/Casks/alfred.rb)). +* Insert a detailed explanation of what you tried to do and why you failed to find the new artifact source. +* After all that **delete all this pre-inserted template text**. + +Failure to follow these instructions may get your issue closed without further explanation. Thank you for taking the time to correctly report the issue. diff --git a/Library/Homebrew/cask/doc/man_page/brew-cask.1.md b/Library/Homebrew/cask/doc/man_page/brew-cask.1.md new file mode 100644 index 0000000000..fc14d54d16 --- /dev/null +++ b/Library/Homebrew/cask/doc/man_page/brew-cask.1.md @@ -0,0 +1,267 @@ +brew-cask(1) - a friendly binary installer for macOS +======================================================== + +## SYNOPSIS + +`brew cask` command [options] [ ... ] + +## DESCRIPTION + +Homebrew-Cask is a tool for installing precompiled macOS binaries (such as +Applications) from the command line. The user is never required to use the +graphical user interface. + +## ALPHA-QUALITY SOFTWARE + +Homebrew-Cask works robustly enough that we welcome new users, but the +project is still in early development. That means command names, option +names, and other aspects of this manual are still subject to change. + +## FREQUENTLY USED COMMANDS + + * `install [--force] [--skip-cask-deps] [--require-sha]` [ ... ]: + Install Cask identified by . + + * `uninstall [--force]` [ ... ]: + Uninstall Cask identified by . + + * `search` | //: + Perform a substring search of known Cask tokens for . If the text + is delimited by slashes, it is interpreted as a Ruby regular expression. + + The tokens returned by `search` are suitable as arguments for most other + commands, such as `install` or `uninstall`. + +## COMMANDS + + * `audit` [ ... ]: + Check the given Casks for installability. + If no tokens are given on the command line, all Casks are audited. + + * `cat` [ ... ]: + Dump the given Cask definition file to the standard output. + + * `cleanup` [--outdated]: + Clean up cached downloads and tracker symlinks. With `--outdated`, + only clean up cached downloads older than 10 days old. + + * `create` : + Generate a Cask definition file for the Cask identified by + and open a template for it in your favorite editor. + + * `doctor` or `dr`: + Check for configuration issues. Can be useful to upload as a gist for + developers along with a bug report. + + * `edit` : + Open the given Cask definition file for editing. + + * `fetch` [--force] [ ... ]: + Download remote application files for the given Cask to the local + cache. With `--force`, force re-download even if the files are already + cached. + + * `home` or `homepage` [ ... ]: + Display the homepage associated with a given Cask in a browser. + + With no arguments, display the project page . + + * `info` or `abv` [ ... ]: + Display information about the given Cask. + + * `install [--force] [--skip-cask-deps] [--require-sha]` [ ... ]: + Install the given Cask. With `--force`, re-install even if the Cask + appears to be already present. With `--skip-cask-deps`, skip any Cask + dependencies. `--require-sha` will abort installation if the Cask does not + have a checksum defined. + + is usually the ID of a Cask as returned by `brew cask search`, + but see [OTHER WAYS TO SPECIFY A CASK][] for variations. + + * `list` or `ls` [-1 | -l] [ ... ]: + Without any arguments, list all installed Casks. With `-1`, always + format the output in a single column. With `-l`, give a more detailed + listing. + + If is given, summarize the staged files associated with the + given Cask. + + * `search` or `-S`: + Display all Casks available for install. + + * `search` or `-S` | //: + Perform a substring search of known Cask tokens for . If the text + is delimited by slashes, it is interpreted as a Ruby regular expression. + + * `style` [--fix] [ ... ]: + Check the given Casks for correct style using [RuboCop Cask](https://github.com/caskroom/rubocop-cask). + If no tokens are given on the command line, all Casks are checked. + With `--fix`, auto-correct any style errors if possible. + + * `uninstall [--force]` or `rm` or `remove` [ ... ]: + Uninstall the given Cask. With `--force`, uninstall even if the Cask + does not appear to be present. + + Note that `uninstall --force` is currently imperfect. It will follow + the `uninstall` instructions from *newest* Cask definition, even if + the given Cask has changed since you installed it. The result is that + `uninstall --force` will always succeed in removing relevant files + under ``, but will sometimes fail to remove relevant + installed files outside of it. This issue is being + addressed. + + `uninstall` without `--force` is also imperfect. It may be unable to + perform an `uninstall` operation if the given Cask has changed since you + installed it. This issue is being addressed. + + * `update`: + For convenience. `brew cask update` is a synonym for `brew update`. + + * `zap` [ ... ]: + Unconditionally remove _all_ files associated with the given Cask. + + Implicitly performs all actions associated with `uninstall`, even if + the Cask does not appear to be currently installed. + + Removes all staged versions of the Cask distribution found under + `/`. + + If the Cask definition contains a `zap` stanza, performs additional + `zap` actions as defined there, such as removing local preference + files. `zap` actions are variable, depending on the level of detail + defined by the Cask author. + + **`zap` may remove files which are shared between applications.** + +## OPTIONS + +To make these options persistent, see the ENVIRONMENT section, below. + +Some of these (such as `--prefpanedir`) may be subject to removal +in a future version. + + * `--force`: + Force an install to proceed even when a previously-existing install + is detected. + + * `--skip-cask-deps`: + Skip Cask dependencies when installing. + + * `--require-sha`: + Abort Cask installation if the Cask does not have a checksum defined. + + * `--caskroom=`: + Location of the Caskroom, where all binaries are stored. The default value is: `$(brew --repository)/Caskroom`. + + * `--verbose`: + Give additional feedback during installation. + + * `--appdir=`: + Target location for Applications. The default value is `/Applications`. + + * `--colorpickerdir=`: + Target location for Color Pickers. The default value is `~/Library/ColorPickers`. + + * `--prefpanedir=`: + Target location for Preference Panes. The default value is `~/Library/PreferencePanes`. + + * `--qlplugindir=`: + Target location for QuickLook Plugins. The default value is `~/Library/QuickLook`. + + * `--fontdir=`: + Target location for Fonts. The default value is `~/Library/Fonts`. + + * `--servicedir=`: + Target location for Services. The default value is `~/Library/Services`. + + * `--input_methoddir=`: + Target location for Input Methods. The default value is `~/Library/Input Methods`. + + * `--internet_plugindir=`: + Target location for Internet Plugins. The default value is `~/Library/Internet Plug-Ins`. + + * `--audio_unit_plugindir=`: + Target location for Audio Unit Plugins. The default value is `~/Library/Audio/Plug-Ins/Components`. + + * `--vst_plugindir=`: + Target location for VST Plugins. The default value is `~/Library/Audio/Plug-Ins/VST`. + + * `--vst3_plugindir=`: + Target location for VST3 Plugins. The default value is `~/Library/Audio/Plug-Ins/VST3`. + + * `--screen_saverdir=`: + Target location for Screen Savers. The default value is `~/Library/Screen Savers`. + + * `--no-binaries`: + Do not link "helper" executables to `/usr/local/bin`. + + * `--debug`: + Output debugging information of use to Cask authors and developers. + +## INTERACTION WITH HOMEBREW + +Homebrew-Cask is implemented as a external command for Homebrew. That means +this project is entirely built upon the Homebrew infrastructure. For +example, upgrades to the Homebrew-Cask tool are received through Homebrew: + + brew update; brew cleanup; brew cask cleanup + +And updates to individual Cask definitions are received whenever you issue +the Homebrew command: + + brew update + +## OTHER WAYS TO SPECIFY A CASK + +Most Homebrew-Cask commands can accept a Cask token as an argument. As +described above, the argument can take the form of: + + * A token as returned by `brew cask search`, _eg_ `google-chrome` + +Homebrew-Cask also accepts three other forms in place of plain tokens: + + * A fully-qualified token which includes the Tap name, _eg_ + `caskroom/fonts/font-symbola` + + * A fully-qualified pathname to a Cask file, _eg_ + `/usr/local/Library/Taps/caskroom/homebrew-cask/Casks/google-chrome.rb` + + * A `curl`-retrievable URI to a Cask file, _eg_ + `https://raw.githubusercontent.com/caskroom/homebrew-cask/f25b6babcd398abf48e33af3d887b2d00de1d661/Casks/google-chrome.rb` + +## ENVIRONMENT + +Homebrew-Cask respects many of the environment variables used by the +parent command `brew`. Please refer to the `brew`(1) man page for more +information. + +Environment variables specific to Homebrew-Cask: + + * HOMEBREW\_CASK\_OPTS: + This variable may contain any arguments normally used as options on + the command-line. This is particularly useful to make options persistent. + For example, you might add to your .bash_profile or .zshenv something like: + `export HOMEBREW_CASK_OPTS='--appdir=/Applications --caskroom=/etc/Caskroom'`. + +## SEE ALSO + +The Homebrew-Cask home page: . + +The Homebrew-Cask GitHub page: . + +`brew`(1), `curl`(1) + +## AUTHORS + +Paul Hinze and Contributors. + +Man page format based on `brew.1.md` from Homebrew. + +## BUGS + +We still have bugs — and we are busy fixing them! If you have a problem, don’t +be shy about reporting it on our [GitHub issues page](https://github.com/caskroom/homebrew-cask/issues?state=open). + +When reporting bugs, remember that Homebrew-Cask is an independent project from +Homebrew. Do your best to direct bug reports to the appropriate project. If +your command-line started with `brew cask`, bring the bug to us first! diff --git a/Library/Homebrew/cask/doc/readme.md b/Library/Homebrew/cask/doc/readme.md new file mode 100644 index 0000000000..cc3bbd73ba --- /dev/null +++ b/Library/Homebrew/cask/doc/readme.md @@ -0,0 +1,12 @@ +# Cask Language Reference + ++ [Synopsis](cask_language_reference/readme.md) ++ [Token reference](cask_language_reference/token_reference.md) ++ [Stanza list](cask_language_reference/all_stanzas.md) ++ [Stanza documentation](cask_language_reference/stanzas/) + +# Contributing + ++ [Contributing to the core](development/hacking.md) ++ [Adding a new Cask](development/adding_a_cask.md) ++ [Meta information on the project’s structure](development/maintaining.md) diff --git a/Library/Homebrew/cask/doc/reporting_bugs/a_cask_fails_to_install.md b/Library/Homebrew/cask/doc/reporting_bugs/a_cask_fails_to_install.md new file mode 100644 index 0000000000..90dc685f0a --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/a_cask_fails_to_install.md @@ -0,0 +1,79 @@ +[Go back](../../README.md#reporting-bugs) + +# A cask fails to install + +Possible reasons: + +* [`curl` error](#curl-error) +* [`Permission denied` error](#permission-denied-error) +* [`sha256 mismatch` error](#sha256-mismatch-error) +* [`source is not there` error](#source-is-not-there-error) +* [Unlisted reason](#unlisted-reason) + +--- + +#### `curl` error: + +If the error output includes something like + +``` +curl: (22) The requested URL returned error: 403 Forbidden +``` + +the problem was with the downloading itself (see how the error came from `curl`). Homebrew-Cask itself is fine and the problem is generally one of: + +1. App vendor or file in their server is down. +2. Cask is outdated. +3. A problem in your setup or connection. + +[Continue to the fix](curl_error_fix_curlrc.md) + +--- + +#### `Permission denied` error: + +If the error output includes something like + +``` +Error: Permission denied - (/usr/local/Caskroom/someapp/0.1/Someapp.app, /Applications/Someapp.app) +``` + +the problem isn’t with Homebrew-Cask itself, but some permissions on your system. + +[Continue to the fix](permission_denied_error_fix_appdir.md) + +--- + +#### `sha256 mismatch` error + +If the error output includes something like + +``` +Error: sha256 mismatch +Expected: 3dbc6c2205af35db5370c7642b9a2b833668880569b9c64a7f5a670bf9911130 +Actual: 526d747d99a93b760f7965e25a57ed61de9b93d566a0ba0c5f1c7e83719b20fd +``` + +Either your download was incomplete/corrupt or the cask is outdated. + +[Continue to the fix](sha256_mismatch_error_fix_icomplete.md) + +--- + +#### `source is not there` error + +If the error output includes something like + +``` +Error: It seems the App source is not there: '/usr/local/Caskroom/…' +``` + +the directory structure inside the app’s archive changed in some way. It must be fixed in Homebrew-Cask. + +[Continue to the fix](source_is_not_there_fix.md) + +--- + +#### Unlisted reason + +If your issue isn’t listed here, [go back](../../README.md#reporting-bugs) and pick `My problem isn’t listed`. diff --git a/Library/Homebrew/cask/doc/reporting_bugs/brew_cask_list_shows_wrong_information.md b/Library/Homebrew/cask/doc/reporting_bugs/brew_cask_list_shows_wrong_information.md new file mode 100644 index 0000000000..981b8cb473 --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/brew_cask_list_shows_wrong_information.md @@ -0,0 +1,7 @@ +[Go back](../../README.md#reporting-bugs) + +# `brew cask list` shows wrong information + +We know. [`brew cask list` is broken](https://github.com/caskroom/homebrew-cask/issues/14058) and has been for quite a while. Pull requests to fix the outstanding problems are welcome. + +**Do not open an issue.** diff --git a/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_curlrc.md b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_curlrc.md new file mode 100644 index 0000000000..cb74139faf --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_curlrc.md @@ -0,0 +1,5 @@ +[Go back](a_cask_fails_to_install.md#curl-error) + +First, lets tackle a common problem: do you have a `.curlrc` file? Those are a frequent cause of issues of this nature. Before anything else, remove that file and try again. If it now works, do not open an issue. Incompatible `.curlrc` configurations must be fixed on your side. + +If, however, you do not have a `.curlrc` or removing it did not work, lets [continue our diagnose](curl_error_fix_vendor.md). diff --git a/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_no_download.md b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_no_download.md new file mode 100644 index 0000000000..2ec686d604 --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_no_download.md @@ -0,0 +1,10 @@ +[Go back](curl_error_fix_vendor.md) + +There are a few reasons why you might not be finding a download: + +* You can’t access the vendor’s website at all. [Continue here](curl_error_fix_wont_fix.md). +* You can access the vendor’s site but they’re informing you the app is now discontinued. Submit a pull request deleting the cask or [open an issue informing us of the situation][issue_dead_app]. +* You can access the vendor’s site but you cannot find the download link. [Open an issue][issue_outdated_cask_but_cannot_find_link] and detail all your steps so far. + +[issue_dead_app]: https://github.com/caskroom/homebrew-cask/issues/new?title=Dead%20app%3A%20&body=%0A%2A%20Insert%20the%20name%20of%20the%20cask%20in%20the%20title%2C%20after%20the%20%60%3A%60.%0A%2A%20Insert%20the%20name%20of%20the%20cask%20and%20a%20link%20to%20it%20in%20the%20body%20of%20this%20issue%20%28example%3A%20%5B%60alfred%60%5D%28https%3A%2F%2Fgithub.com%2Fcaskroom%2Fhomebrew-cask%2Fblob%2Fmaster%2FCasks%2Falfred.rb%29%29.%0A%2A%20Insert%20a%20link%20to%20the%20page%20that%20informs%20about%20the%20app%20being%20dead.%0A%2A%20After%20all%20that%20%2A%2Adelete%20all%20this%20pre-inserted%20template%20text%2A%2A.%0A%0AFailure%20to%20follow%20these%20instructions%20may%20get%20your%20issue%20closed%20without%20further%20explanation.%20Thank%20you%20for%20taking%20the%20time%20to%20correctly%20report%20the%20issue. +[issue_outdated_cask_but_cannot_find_link]: https://github.com/caskroom/homebrew-cask/issues/new?title=Outdated%20cask%20but%20cannot%20find%20link%3A%20&body=%0A%2A%20Insert%20the%20name%20of%20the%20cask%20in%20the%20title%2C%20after%20the%20%60%3A%60.%0A%2A%20Insert%20the%20name%20of%20the%20cask%20and%20a%20link%20to%20it%20in%20the%20body%20of%20this%20issue%20%28example%3A%20%5B%60alfred%60%5D%28https%3A%2F%2Fgithub.com%2Fcaskroom%2Fhomebrew-cask%2Fblob%2Fmaster%2FCasks%2Falfred.rb%29%29.%0A%2A%20Insert%20a%20detailed%20explanation%20of%20what%20you%20tried%20to%20do%20and%20why%20you%20failed%20to%20find%20the%20download%20link.%0A%2A%20After%20all%20that%20%2A%2Adelete%20all%20this%20pre-inserted%20template%20text%2A%2A.%0A%0AFailure%20to%20follow%20these%20instructions%20may%20get%20your%20issue%20closed%20without%20further%20explanation.%20Thank%20you%20for%20taking%20the%20time%20to%20correctly%20report%20the%20issue. diff --git a/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_outdated.md b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_outdated.md new file mode 100644 index 0000000000..6a7e6e73f1 --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_outdated.md @@ -0,0 +1,11 @@ +[Go back](curl_error_fix_vendor.md) + +Since the download started, it likely means the cask is outdated. Lets fix it: + +1. Look around the app’s website and find out what the latest version is. It will likely be expressed in the URL used to download it. +2. Take a look at the cask’s version (`brew cask _stanza version {{cask_name}}`) and verify it is indeed outdated. + * If the app’s version is `latest`, it means the `url` itself is outdated. It will need to be changed to the new one. + +If it is outdated, start by trying to [submit a fix](../../CONTRIBUTING.md#updating-a-cask). If you’re having trouble, [open an issue][issue_outdated_cask] explaining your steps so far and why you’re having trouble submitting the update. + +[issue_outdated_cask]: https://github.com/caskroom/homebrew-cask/issues/new?title=Outdated%20cask%20but%20cannot%20find%20link%3A%20&body=%0A%2A%20Insert%20the%20name%20of%20the%20cask%20in%20the%20title%2C%20after%20the%20%60%3A%60.%0A%2A%20Insert%20the%20name%20of%20the%20cask%20and%20a%20link%20to%20it%20in%20the%20body%20of%20this%20issue%20%28example%3A%20%5B%60alfred%60%5D%28https%3A%2F%2Fgithub.com%2Fcaskroom%2Fhomebrew-cask%2Fblob%2Fmaster%2FCasks%2Falfred.rb%29%29.%0A%2A%20Insert%20a%20detailed%20explanation%20of%20what%20you%20tried%20to%20do%20and%20why%20you%20failed%20to%20find%20the%20download%20link.%0A%2A%20After%20all%20that%20%2A%2Adelete%20all%20this%20pre-inserted%20template%20text%2A%2A.%0A%0AFailure%20to%20follow%20these%20instructions%20may%20get%20your%20issue%20closed%20without%20further%20explanation.%20Thank%20you%20for%20taking%20the%20time%20to%20correctly%20report%20the%20issue. diff --git a/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_vendor.md b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_vendor.md new file mode 100644 index 0000000000..bcb785ba79 --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_vendor.md @@ -0,0 +1,8 @@ +[Go back](curl_error_fix_curlrc) + +Lets now see if the issue is upstream: + +1. Got to the vendor’s website (`brew cask home {{cask_name}}`). +2. Find the download link for the app and click on it. + +Does it download? [Yes](curl_error_fix_outdated.md) | [No](curl_error_fix_wont_fix.md) | [There is nothing to download](curl_error_fix_no_download.md) diff --git a/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_wont_fix.md b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_wont_fix.md new file mode 100644 index 0000000000..c02b9adfec --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/curl_error_fix_wont_fix.md @@ -0,0 +1,9 @@ +[Go back](curl_error_fix_vendor.md) + +This means the issue isn’t in any way related to Homebrew-Cask, but with the vendor or your connection. + +Start by diagnosing your connection (try to download other casks, go around the web). If the problem is with your connection, try a website like [Ask Different](https://apple.stackexchange.com/) to ask for advice. + +If you’re sure the issue is not with your connection, contact the app’s vendor and let them know their link is down, so they can fix it. + +**Do not open an issue.** diff --git a/Library/Homebrew/cask/doc/reporting_bugs/permission_denied_error_fix_appdir.md b/Library/Homebrew/cask/doc/reporting_bugs/permission_denied_error_fix_appdir.md new file mode 100644 index 0000000000..fcee7fd738 --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/permission_denied_error_fix_appdir.md @@ -0,0 +1,7 @@ +[Go back](a_cask_fails_to_install.md#permission-denied-error) + +In this case, it’s likely you’re a standard user and don’t have permissions to write to `/Applications` (which is now our default). If you never changed your default installation directory for apps and were using Homebrew-Cask before we changed our behaviour to moving apps instead of linking, you should have some symlinks in your `~/Applications` directory, which you do have permission to write to. + +You can use `--appdir=~/Applications` when installing to bring back the old behaviour. For a permanent change, [follow the `Options` section in `USAGE.md`](https://github.com/caskroom/homebrew-cask/blob/1de4657a0ed35463602b31061b0c16dc9078b8a0/USAGE.md#options). Specifically, you’ll want to set `export HOMEBREW_CASK_OPTS="--appdir=~/Applications"` in your shell’s startup file. + +If `--appdir` doesn’t fix the issue or you do have write permissions to `/Applications`, the problem may lie [in the app bundle itself](permission_denied_error_fix_bundle.md). diff --git a/Library/Homebrew/cask/doc/reporting_bugs/permission_denied_error_fix_bundle.md b/Library/Homebrew/cask/doc/reporting_bugs/permission_denied_error_fix_bundle.md new file mode 100644 index 0000000000..7b23eef0ad --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/permission_denied_error_fix_bundle.md @@ -0,0 +1,5 @@ +[Go back](permission_denied_error_fix_appdir.md) + +Some app bundles don’t have certain permissions that might be necessary for us to move them to the appropriate location. You may check such permissions with `ls -ls {{path_to_app_bundle}}`. If you see something like `dr-xr-xr-x` at the start of the output, that may be the cause. To fix it, we simply change the app bundle’s permission to allow us to move it, and then set it back to what it was (in case the developer set those permissions deliberately). See [`licoin`](https://github.com/caskroom/homebrew-cask/blob/0cde71f1fea8ad62d6ec4732fcf35ac0c52d8792/Casks/litecoin.rb#L14L20) for an example of such a cask. + +If the issue persists, [go back](../../README.md#reporting-bugs) and pick `My problem isn’t listed`. Be sure to mention all your steps so far and what you’re having difficulties with. diff --git a/Library/Homebrew/cask/doc/reporting_bugs/pre_bug_report.md b/Library/Homebrew/cask/doc/reporting_bugs/pre_bug_report.md new file mode 100644 index 0000000000..4a7fba3c48 --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/pre_bug_report.md @@ -0,0 +1,15 @@ +Before reporting a bug, lets make sure everything is right with your setup. + +When reporting bugs, remember that Homebrew-Cask is an independent project from Homebrew. Do your best to direct bug reports to the appropriate project. If your command started with `brew cask`, bring the bug to us first. + +Start by searching for your issue before posting a new one. If you find an open issue and have any new information not reported in the original, please add your insights. If you find a closed issue, try the solutions there. If the issue is still not solved, open a new one with your new information and a link back to the old related issue. + +If you did not find your particular bug, before reporting it make sure you have the latest versions of Homebrew, Homebrew-Cask, and all Taps by running the following commands. These will also fix some other issues: + +```bash +$ brew update; brew cleanup; brew cask cleanup +$ brew uninstall --force brew-cask; brew update +$ brew untap phinze/cask; brew untap caskroom/cask; brew update +``` + +Retry your failing command. If the issue persists, [go back](../../README.md#reporting-bugs) and pick the appropriate instructions for your problem. diff --git a/Library/Homebrew/cask/doc/reporting_bugs/sha256_mismatch_error_fix_icomplete.md b/Library/Homebrew/cask/doc/reporting_bugs/sha256_mismatch_error_fix_icomplete.md new file mode 100644 index 0000000000..7da4883340 --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/sha256_mismatch_error_fix_icomplete.md @@ -0,0 +1,5 @@ +[Go back](a_cask_fails_to_install.md#sha256-mismatch-error) + +First, lets see if the problem was with your download. Delete the downloaded file (its location will be pointed out right under the `Actual` shasum line) and try again. + +If the problem persists, [the cask must be outdated](sha256_mismatch_error_fix_outdated.md). diff --git a/Library/Homebrew/cask/doc/reporting_bugs/sha256_mismatch_error_fix_outdated.md b/Library/Homebrew/cask/doc/reporting_bugs/sha256_mismatch_error_fix_outdated.md new file mode 100644 index 0000000000..a37ea53378 --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/sha256_mismatch_error_fix_outdated.md @@ -0,0 +1,13 @@ +[Go back](sha256_mismatch_error_fix_icomplete.md) + +Lets bring the cask up to date. It’ll likely need a new version, but it’s possible the version has remained the same (happens occasionally when the vendor updates the app in place). + +1. Got to the vendor’s website (`brew cask home {{cask_name}}`). + * Alternatively, if it has an `appcast`, read that (`curl "$(brew cask _stanza appcast {{cask_name}})"`). +2. Find out what the latest version is. It will likely be expressed in the URL used to download it. +3. Take a look at the cask’s version (`brew cask _stanza version {{cask_name}}`) and verify it is indeed outdated. + * If the app’s version is `latest`, it means the `url` itself is outdated. It will need to be changed to the new one. + +If it is outdated, start by trying to [submit a fix](../../CONTRIBUTING.md#updating-a-cask). If you’re having trouble, [open an issue][issue_outdated_cask] explaining your steps so far and why you’re having trouble submitting the update. + +[issue_outdated_cask]: https://github.com/caskroom/homebrew-cask/issues/new?title=Outdated%20cask%20but%20cannot%20find%20link%3A%20&body=%0A%2A%20Insert%20the%20name%20of%20the%20cask%20in%20the%20title%2C%20after%20the%20%60%3A%60.%0A%2A%20Insert%20the%20name%20of%20the%20cask%20and%20a%20link%20to%20it%20in%20the%20body%20of%20this%20issue%20%28example%3A%20%5B%60alfred%60%5D%28https%3A%2F%2Fgithub.com%2Fcaskroom%2Fhomebrew-cask%2Fblob%2Fmaster%2FCasks%2Falfred.rb%29%29.%0A%2A%20Insert%20a%20detailed%20explanation%20of%20what%20you%20tried%20to%20do%20and%20why%20you%20failed%20to%20find%20the%20download%20link.%0A%2A%20After%20all%20that%20%2A%2Adelete%20all%20this%20pre-inserted%20template%20text%2A%2A.%0A%0AFailure%20to%20follow%20these%20instructions%20may%20get%20your%20issue%20closed%20without%20further%20explanation.%20Thank%20you%20for%20taking%20the%20time%20to%20correctly%20report%20the%20issue. diff --git a/Library/Homebrew/cask/doc/reporting_bugs/source_is_not_there_fix.md b/Library/Homebrew/cask/doc/reporting_bugs/source_is_not_there_fix.md new file mode 100644 index 0000000000..8ee7c31e9c --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/source_is_not_there_fix.md @@ -0,0 +1,28 @@ +[Go back](a_cask_fails_to_install.md#source-is-not-there-error) + +First, you need to identify which artifact is not being handled correctly anymore. It’s explicit in the error message: if it says `Error: It seems the App source…'` the problem is [`app`](https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/app.md). The pattern is the same across all artifacts. + +Fixing this error is typically easy, and requires only a bit of time on your part. Start by downloading the package for the cask: `brew cask fetch {{cask_name}}`. The last line of output will inform you of the location of the download. Navigate there and manually unpack it. As an example, lets say the structure inside the archive is as follows: + +``` +. +├─ Files/SomeApp.app +├─ Files/script.sh +└─ README.md +``` + +Now, lets look at the cask (`brew cask cat {{cask_name}}`: + +``` +(…) +app 'SomeApp.app' +(…) +``` + +The cask was expecting `SomeApp.app` to be in the top directory of the archive (see how it says simply `SomeApp.app`) but the developer changed it to inside a `Files` directory. All we have to do is update that line of the cask to follow the new structure: `app 'Files/SomeApp.app'`. + +Note that occasionally the app’s name changes completely (from `SomeApp.app` to `OtherApp.app`, lets say). In these instances, the filename of the cask itself, as well as its token, must also change. Consult the [`token reference`](https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/token_reference.md) for complete instructions on the new name. + +Follow the instructions to [submit a fix](../../CONTRIBUTING.md#updating-a-cask), and make the change to the cask. If using `cask-repair`, you may give it the `--edit-cask` flag when calling it on the command-line to step right into editing it. If you’re having trouble, [open an issue][issue_source_not_there_and_cannot_find_it] explaining your steps so far and why you’re having trouble submitting the update. + +[issue_source_not_there_and_cannot_find_it]: https://github.com/caskroom/homebrew-cask/issues/new?title=Source%20not%20there%20and%20cannot%20find%20it%3A%20&body=%0A%2A%20Insert%20the%20name%20of%20the%20cask%20in%20the%20title%2C%20after%20the%20%60%3A%60.%0A%2A%20Insert%20the%20name%20of%20the%20cask%20and%20a%20link%20to%20it%20in%20the%20body%20of%20this%20issue%20%28example%3A%20%5B%60alfred%60%5D%28https%3A%2F%2Fgithub.com%2Fcaskroom%2Fhomebrew-cask%2Fblob%2Fmaster%2FCasks%2Falfred.rb%29%29.%0A%2A%20Insert%20a%20detailed%20explanation%20of%20what%20you%20tried%20to%20do%20and%20why%20you%20failed%20to%20find%20the%20new%20artifact%20source.%0A%2A%20After%20all%20that%20%2A%2Adelete%20all%20this%20pre-inserted%20template%20text%2A%2A.%0A%0AFailure%20to%20follow%20these%20instructions%20may%20get%20your%20issue%20closed%20without%20further%20explanation.%20Thank%20you%20for%20taking%20the%20time%20to%20correctly%20report%20the%20issue. \ No newline at end of file diff --git a/Library/Homebrew/cask/doc/reporting_bugs/uninstall_wrongly_reports_cask_as_not_installed.md b/Library/Homebrew/cask/doc/reporting_bugs/uninstall_wrongly_reports_cask_as_not_installed.md new file mode 100644 index 0000000000..1b1227de85 --- /dev/null +++ b/Library/Homebrew/cask/doc/reporting_bugs/uninstall_wrongly_reports_cask_as_not_installed.md @@ -0,0 +1,11 @@ +[Go back](../../README.md#reporting-bugs) + +# `uninstall` wrongly reports cask as not installed + +Reports of this bug are usually accompanied with the output of `brew cask list` showing the cask is indeed installed. [You should not rely on `brew cask list`](https://github.com/caskroom/homebrew-cask/issues/14058) anyway, and this error is somewhat related to it. + +This is prone to happen when you try to uninstall a cask that has been updated since you last installed, or when a cask no longer exists in the repo (`brew cask list` will show it with `(!)`). We’re working to handle those situations better. + +For now, rerun your command with `--force`, or uninstall manually. + +**Do not open an issue.** diff --git a/Library/Homebrew/cask/lib/hbc.rb b/Library/Homebrew/cask/lib/hbc.rb new file mode 100644 index 0000000000..a9a23f9972 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc.rb @@ -0,0 +1,60 @@ +module Hbc; end + +require "hardware" +require "hbc/extend" +require "hbc/artifact" +require "hbc/audit" +require "hbc/auditor" +require "hbc/cache" +require "hbc/cask" +require "hbc/without_source" +require "hbc/caskroom" +require "hbc/checkable" +require "hbc/cli" +require "hbc/cask_dependencies" +require "hbc/caveats" +require "hbc/container" +require "hbc/download" +require "hbc/download_strategy" +require "hbc/exceptions" +require "hbc/fetcher" +require "hbc/installer" +require "hbc/locations" +require "hbc/macos" +require "hbc/options" +require "hbc/pkg" +require "hbc/qualified_token" +require "hbc/scopes" +require "hbc/source" +require "hbc/staged" +require "hbc/system_command" +require "hbc/topological_hash" +require "hbc/underscore_supporting_uri" +require "hbc/url" +require "hbc/url_checker" +require "hbc/utils" +require "hbc/verify" +require "hbc/version" + +require "vendor/plist" + +module Hbc + include Hbc::Locations + include Hbc::Scopes + include Hbc::Options + include Hbc::Utils + + def self.init + Hbc::Cache.ensure_cache_exists + Hbc::Cache.migrate_legacy_cache + + Hbc::Caskroom.ensure_caskroom_exists + end + + def self.load(query) + odebug "Loading Cask definitions" + cask = Hbc::Source.for_query(query).load + cask.dumpcask + cask + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact.rb b/Library/Homebrew/cask/lib/hbc/artifact.rb new file mode 100644 index 0000000000..73bd582a59 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact.rb @@ -0,0 +1,65 @@ +module Hbc::Artifact; end + +require "hbc/artifact/app" +require "hbc/artifact/artifact" # generic 'artifact' stanza +require "hbc/artifact/binary" +require "hbc/artifact/colorpicker" +require "hbc/artifact/font" +require "hbc/artifact/input_method" +require "hbc/artifact/installer" +require "hbc/artifact/internet_plugin" +require "hbc/artifact/audio_unit_plugin" +require "hbc/artifact/vst_plugin" +require "hbc/artifact/vst3_plugin" +require "hbc/artifact/nested_container" +require "hbc/artifact/pkg" +require "hbc/artifact/postflight_block" +require "hbc/artifact/preflight_block" +require "hbc/artifact/prefpane" +require "hbc/artifact/qlplugin" +require "hbc/artifact/screen_saver" +require "hbc/artifact/service" +require "hbc/artifact/stage_only" +require "hbc/artifact/suite" +require "hbc/artifact/uninstall" +require "hbc/artifact/zap" + +module Hbc::Artifact + # NOTE: order is important here, since we want to extract nested containers + # before we handle any other artifacts + def self.artifacts + [ + Hbc::Artifact::PreflightBlock, + Hbc::Artifact::NestedContainer, + Hbc::Artifact::Installer, + Hbc::Artifact::App, + Hbc::Artifact::Suite, + Hbc::Artifact::Artifact, # generic 'artifact' stanza + Hbc::Artifact::Colorpicker, + Hbc::Artifact::Pkg, + Hbc::Artifact::Prefpane, + Hbc::Artifact::Qlplugin, + Hbc::Artifact::Font, + Hbc::Artifact::Service, + Hbc::Artifact::StageOnly, + Hbc::Artifact::Binary, + Hbc::Artifact::InputMethod, + Hbc::Artifact::InternetPlugin, + Hbc::Artifact::AudioUnitPlugin, + Hbc::Artifact::VstPlugin, + Hbc::Artifact::Vst3Plugin, + Hbc::Artifact::ScreenSaver, + Hbc::Artifact::Uninstall, + Hbc::Artifact::PostflightBlock, + Hbc::Artifact::Zap, + ] + end + + def self.for_cask(cask) + odebug "Determining which artifacts are present in Cask #{cask}" + artifacts.select do |artifact| + odebug "Checking for artifact class #{artifact}" + artifact.me?(cask) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/abstract_flight_block.rb b/Library/Homebrew/cask/lib/hbc/artifact/abstract_flight_block.rb new file mode 100644 index 0000000000..fcf98d7adb --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/abstract_flight_block.rb @@ -0,0 +1,36 @@ +require "hbc/artifact/base" + +class Hbc::Artifact::AbstractFlightBlock < Hbc::Artifact::Base + def self.artifact_dsl_key + super.to_s.sub(%r{_block$}, "").to_sym + end + + def self.uninstall_artifact_dsl_key + artifact_dsl_key.to_s.prepend("uninstall_").to_sym + end + + def self.class_for_dsl_key(dsl_key) + Object.const_get("Hbc::DSL::#{dsl_key.to_s.split('_').collect(&:capitalize).join}") + end + + def self.me?(cask) + cask.artifacts[artifact_dsl_key].any? || + cask.artifacts[uninstall_artifact_dsl_key].any? + end + + def install_phase + abstract_phase(self.class.artifact_dsl_key) + end + + def uninstall_phase + abstract_phase(self.class.uninstall_artifact_dsl_key) + end + + private + + def abstract_phase(dsl_key) + @cask.artifacts[dsl_key].each do |block| + self.class.class_for_dsl_key(dsl_key).new(@cask).instance_eval(&block) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/app.rb b/Library/Homebrew/cask/lib/hbc/artifact/app.rb new file mode 100644 index 0000000000..bbda16f744 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/app.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::App < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/artifact.rb b/Library/Homebrew/cask/lib/hbc/artifact/artifact.rb new file mode 100644 index 0000000000..e2c06eb70a --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/artifact.rb @@ -0,0 +1,20 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::Artifact < Hbc::Artifact::Moved + def self.artifact_english_name + "Generic Artifact" + end + + def self.artifact_dirmethod + :appdir + end + + def load_specification(artifact_spec) + source_string, target_hash = artifact_spec + raise Hbc::CaskInvalidError.new(@cask.token, "no source given for artifact") if source_string.nil? + @source = @cask.staged_path.join(source_string) + raise Hbc::CaskInvalidError.new(@cask.token, "target required for generic artifact #{source_string}") unless target_hash.is_a?(Hash) + target_hash.assert_valid_keys(:target) + @target = Pathname.new(target_hash[:target]) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/audio_unit_plugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/audio_unit_plugin.rb new file mode 100644 index 0000000000..7f3999306d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/audio_unit_plugin.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::AudioUnitPlugin < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/base.rb b/Library/Homebrew/cask/lib/hbc/artifact/base.rb new file mode 100644 index 0000000000..9a07cc9066 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/base.rb @@ -0,0 +1,79 @@ +class Hbc::Artifact::Base + def self.artifact_name + @artifact_name ||= name.sub(%r{^.*:}, "").gsub(%r{(.)([A-Z])}, '\1_\2').downcase + end + + def self.artifact_english_name + @artifact_english_name ||= name.sub(%r{^.*:}, "").gsub(%r{(.)([A-Z])}, '\1 \2') + end + + def self.artifact_english_article + @artifact_english_article ||= artifact_english_name =~ %r{^[aeiou]}i ? "an" : "a" + end + + def self.artifact_dsl_key + @artifact_dsl_key ||= artifact_name.to_sym + end + + def self.artifact_dirmethod + @artifact_dirmethod ||= "#{artifact_name}dir".to_sym + end + + def self.me?(cask) + cask.artifacts[artifact_dsl_key].any? + end + + attr_reader :force + + def zap_phase + odebug "Nothing to do. The #{self.class.artifact_name} artifact has no zap phase." + end + + # TODO: this sort of logic would make more sense in dsl.rb, or a + # constructor called from dsl.rb, so long as that isn't slow. + def self.read_script_arguments(arguments, stanza, default_arguments = {}, override_arguments = {}, key = nil) + # TODO: when stanza names are harmonized with class names, + # stanza may not be needed as an explicit argument + description = stanza.to_s + if key + arguments = arguments[key] + description.concat(" #{key.inspect}") + end + + # backward-compatible string value + arguments = { executable: arguments } if arguments.is_a?(String) + + # key sanity + permitted_keys = [:args, :input, :executable, :must_succeed, :sudo, :bsexec, :print_stdout, :print_stderr] + unknown_keys = arguments.keys - permitted_keys + unless unknown_keys.empty? + opoo %Q{Unknown arguments to #{description} -- #{unknown_keys.inspect} (ignored). Running "brew update; brew cleanup; brew cask cleanup" will likely fix it.} + end + arguments.reject! { |k| !permitted_keys.include?(k) } + + # key warnings + override_keys = override_arguments.keys + ignored_keys = arguments.keys & override_keys + unless ignored_keys.empty? + onoe "Some arguments to #{description} will be ignored -- :#{unknown_keys.inspect} (overridden)." + end + + # extract executable + executable = arguments.key?(:executable) ? arguments.delete(:executable) : nil + + arguments = default_arguments.merge arguments + arguments.merge! override_arguments + + [executable, arguments] + end + + def summary + {} + end + + def initialize(cask, command: Hbc::SystemCommand, force: false) + @cask = cask + @command = command + @force = force + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/binary.rb b/Library/Homebrew/cask/lib/hbc/artifact/binary.rb new file mode 100644 index 0000000000..ccaebe0c84 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/binary.rb @@ -0,0 +1,7 @@ +require "hbc/artifact/symlinked" + +class Hbc::Artifact::Binary < Hbc::Artifact::Symlinked + def install_phase + super unless Hbc.no_binaries + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/colorpicker.rb b/Library/Homebrew/cask/lib/hbc/artifact/colorpicker.rb new file mode 100644 index 0000000000..7b56d0ffc9 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/colorpicker.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::Colorpicker < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/font.rb b/Library/Homebrew/cask/lib/hbc/artifact/font.rb new file mode 100644 index 0000000000..9697d9e138 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/font.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::Font < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/input_method.rb b/Library/Homebrew/cask/lib/hbc/artifact/input_method.rb new file mode 100644 index 0000000000..3c7f3d9903 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/input_method.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::InputMethod < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/installer.rb b/Library/Homebrew/cask/lib/hbc/artifact/installer.rb new file mode 100644 index 0000000000..2f66397e92 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/installer.rb @@ -0,0 +1,41 @@ +require "hbc/artifact/base" + +class Hbc::Artifact::Installer < Hbc::Artifact::Base + # TODO: for backward compatibility, removeme + def install + install_phase + end + + # TODO: for backward compatibility, removeme + def uninstall + uninstall_phase + end + + def install_phase + @cask.artifacts[self.class.artifact_dsl_key].each do |artifact| + if artifact.manual + puts <<-EOS.undent + To complete the installation of Cask #{@cask}, you must also + run the installer at + + '#{@cask.staged_path.join(artifact.manual)}' + + EOS + else + executable, script_arguments = self.class.read_script_arguments(artifact.script, + self.class.artifact_dsl_key.to_s, + { must_succeed: true, sudo: true }, + print_stdout: true) + ohai "Running #{self.class.artifact_dsl_key} script #{executable}" + raise Hbc::CaskInvalidError.new(@cask, "#{self.class.artifact_dsl_key} missing executable") if executable.nil? + executable_path = @cask.staged_path.join(executable) + @command.run("/bin/chmod", args: ["--", "+x", executable_path]) if File.exist?(executable_path) + @command.run(executable_path, script_arguments) + end + end + end + + def uninstall_phase + odebug "Nothing to do. The #{self.class.artifact_dsl_key} artifact has no uninstall phase." + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/internet_plugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/internet_plugin.rb new file mode 100644 index 0000000000..a444182748 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/internet_plugin.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::InternetPlugin < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/moved.rb b/Library/Homebrew/cask/lib/hbc/artifact/moved.rb new file mode 100644 index 0000000000..c6b52f30fb --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/moved.rb @@ -0,0 +1,88 @@ +require "hbc/artifact/relocated" + +class Hbc::Artifact::Moved < Hbc::Artifact::Relocated + def self.english_description + "#{artifact_english_name}s" + end + + def install_phase + each_artifact do |artifact| + load_specification(artifact) + next unless preflight_checks + delete if Hbc::Utils.path_occupied?(target) && force + move + end + end + + def uninstall_phase + each_artifact do |artifact| + load_specification(artifact) + next unless File.exist?(target) + delete + end + end + + private + + def each_artifact + # the sort is for predictability between Ruby versions + @cask.artifacts[self.class.artifact_dsl_key].sort.each do |artifact| + yield artifact + end + end + + def move + ohai "Moving #{self.class.artifact_english_name} '#{source.basename}' to '#{target}'" + target.dirname.mkpath + FileUtils.move(source, target) + add_altname_metadata target, source.basename.to_s + end + + def preflight_checks + if Hbc::Utils.path_occupied?(target) + if force + ohai(warning_target_exists { |s| s << "overwriting." }) + else + ohai(warning_target_exists { |s| s << "not moving." }) + return false + end + end + unless source.exist? + message = "It seems the #{self.class.artifact_english_name} source is not there: '#{source}'" + raise Hbc::CaskError, message + end + true + end + + def warning_target_exists + message_parts = [ + "It seems there is already #{self.class.artifact_english_article} #{self.class.artifact_english_name} at '#{target}'", + ] + yield(message_parts) if block_given? + message_parts.join("; ") + end + + def delete + ohai "Removing #{self.class.artifact_english_name}: '#{target}'" + if MacOS.undeletable?(target) + raise Hbc::CaskError, "Cannot remove undeletable #{self.class.artifact_english_name}" + elsif force + Hbc::Utils.gain_permissions_remove(target, command: @command) + else + target.rmtree + end + end + + def summarize_artifact(artifact_spec) + load_specification artifact_spec + + if target.exist? + target_abv = " (#{target.abv})" + else + warning = "Missing #{self.class.artifact_english_name}" + warning = "#{Hbc::Utils::Tty.red.underline}#{warning}#{Hbc::Utils::Tty.reset}: " + end + + "#{warning}#{printable_target}#{target_abv}" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/nested_container.rb b/Library/Homebrew/cask/lib/hbc/artifact/nested_container.rb new file mode 100644 index 0000000000..68e4a552cc --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/nested_container.rb @@ -0,0 +1,24 @@ +require "hbc/artifact/base" + +class Hbc::Artifact::NestedContainer < Hbc::Artifact::Base + def install_phase + @cask.artifacts[:nested_container].each { |container| extract(container) } + end + + def uninstall_phase + # no need to take action; is removed after extraction + end + + def extract(container_relative_path) + source = @cask.staged_path.join(container_relative_path) + container = Hbc::Container.for_path(source, @command) + + unless container + raise Hbc::CaskError, "Aw dang, could not identify nested container at '#{source}'" + end + + ohai "Extracting nested container #{source.basename}" + container.new(@cask, source, @command).extract + FileUtils.remove_entry_secure(source) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/pkg.rb b/Library/Homebrew/cask/lib/hbc/artifact/pkg.rb new file mode 100644 index 0000000000..fb27308d74 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/pkg.rb @@ -0,0 +1,53 @@ +require "hbc/artifact/base" + +class Hbc::Artifact::Pkg < Hbc::Artifact::Base + attr_reader :pkg_relative_path + + def self.artifact_dsl_key + :pkg + end + + def load_pkg_description(pkg_description) + @pkg_relative_path = pkg_description.shift + @pkg_install_opts = pkg_description.shift + begin + if @pkg_install_opts.respond_to?(:keys) + @pkg_install_opts.assert_valid_keys(:allow_untrusted) + elsif @pkg_install_opts + raise + end + raise if pkg_description.nil? + rescue StandardError + raise Hbc::CaskInvalidError.new(@cask, "Bad pkg stanza") + end + end + + def pkg_install_opts(opt) + @pkg_install_opts[opt] if @pkg_install_opts.respond_to?(:keys) + end + + def install_phase + @cask.artifacts[:pkg].each { |pkg_description| run_installer(pkg_description) } + end + + def uninstall_phase + # Do nothing. Must be handled explicitly by a separate :uninstall stanza. + end + + def run_installer(pkg_description) + load_pkg_description pkg_description + ohai "Running installer for #{@cask}; your password may be necessary." + ohai "Package installers may write to any location; options such as --appdir are ignored." + source = @cask.staged_path.join(pkg_relative_path) + unless source.exist? + raise Hbc::CaskError, "pkg source file not found: '#{source}'" + end + args = [ + "-pkg", source, + "-target", "/" + ] + args << "-verboseR" if Hbc.verbose + args << "-allowUntrusted" if pkg_install_opts :allow_untrusted + @command.run!("/usr/sbin/installer", sudo: true, args: args, print_stdout: true) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/postflight_block.rb b/Library/Homebrew/cask/lib/hbc/artifact/postflight_block.rb new file mode 100644 index 0000000000..92b21a83f9 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/postflight_block.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/abstract_flight_block" + +class Hbc::Artifact::PostflightBlock < Hbc::Artifact::AbstractFlightBlock +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/preflight_block.rb b/Library/Homebrew/cask/lib/hbc/artifact/preflight_block.rb new file mode 100644 index 0000000000..772a88016d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/preflight_block.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/abstract_flight_block" + +class Hbc::Artifact::PreflightBlock < Hbc::Artifact::AbstractFlightBlock +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/prefpane.rb b/Library/Homebrew/cask/lib/hbc/artifact/prefpane.rb new file mode 100644 index 0000000000..e45cc0b19d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/prefpane.rb @@ -0,0 +1,7 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::Prefpane < Hbc::Artifact::Moved + def self.artifact_english_name + "Preference Pane" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/qlplugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/qlplugin.rb new file mode 100644 index 0000000000..6702aa5ef9 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/qlplugin.rb @@ -0,0 +1,21 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::Qlplugin < Hbc::Artifact::Moved + def self.artifact_english_name + "QuickLook Plugin" + end + + def install_phase + super + reload_quicklook + end + + def uninstall_phase + super + reload_quicklook + end + + def reload_quicklook + @command.run!("/usr/bin/qlmanage", args: ["-r"]) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/relocated.rb b/Library/Homebrew/cask/lib/hbc/artifact/relocated.rb new file mode 100644 index 0000000000..cd0054188c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/relocated.rb @@ -0,0 +1,53 @@ +require "hbc/artifact/base" + +class Hbc::Artifact::Relocated < Hbc::Artifact::Base + def summary + { + english_description: self.class.english_description, + contents: @cask.artifacts[self.class.artifact_dsl_key].map(&method(:summarize_artifact)).compact, + } + end + + attr_reader :source, :target + + def printable_target + target.to_s.sub(%r{^#{ENV['HOME']}(#{File::SEPARATOR}|$)}, "~/") + end + + ALT_NAME_ATTRIBUTE = "com.apple.metadata:kMDItemAlternateNames".freeze + + # Try to make the asset searchable under the target name. Spotlight + # respects this attribute for many filetypes, but ignores it for App + # bundles. Alfred 2.2 respects it even for App bundles. + def add_altname_metadata(file, altname) + return if altname.casecmp(file.basename).zero? + odebug "Adding #{ALT_NAME_ATTRIBUTE} metadata" + altnames = @command.run("/usr/bin/xattr", + args: ["-p", ALT_NAME_ATTRIBUTE, file.to_s], + print_stderr: false).stdout.sub(%r{\A\((.*)\)\Z}, '\1') + odebug "Existing metadata is: '#{altnames}'" + altnames.concat(", ") unless altnames.empty? + altnames.concat(%Q{"#{altname}"}) + altnames = "(#{altnames})" + + # Some packges are shipped as u=rx (e.g. Bitcoin Core) + @command.run!("/bin/chmod", args: ["--", "u=rwx", file.to_s, file.realpath.to_s]) + + @command.run!("/usr/bin/xattr", + args: ["-w", ALT_NAME_ATTRIBUTE, altnames, file.to_s], + print_stderr: false) + end + + def load_specification(artifact_spec) + source_string, target_hash = artifact_spec + raise Hbc::CaskInvalidError if source_string.nil? + @source = @cask.staged_path.join(source_string) + if target_hash + raise Hbc::CaskInvalidError unless target_hash.respond_to?(:keys) + target_hash.assert_valid_keys(:target) + @target = Hbc.send(self.class.artifact_dirmethod).join(target_hash[:target]) + else + @target = Hbc.send(self.class.artifact_dirmethod).join(source.basename) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/screen_saver.rb b/Library/Homebrew/cask/lib/hbc/artifact/screen_saver.rb new file mode 100644 index 0000000000..bbd9291527 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/screen_saver.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::ScreenSaver < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/service.rb b/Library/Homebrew/cask/lib/hbc/artifact/service.rb new file mode 100644 index 0000000000..d5a00e4fe0 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/service.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::Service < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/stage_only.rb b/Library/Homebrew/cask/lib/hbc/artifact/stage_only.rb new file mode 100644 index 0000000000..7a48b19aa1 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/stage_only.rb @@ -0,0 +1,15 @@ +require "hbc/artifact/base" + +class Hbc::Artifact::StageOnly < Hbc::Artifact::Base + def self.artifact_dsl_key + :stage_only + end + + def install_phase + # do nothing + end + + def uninstall_phase + # do nothing + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/suite.rb b/Library/Homebrew/cask/lib/hbc/artifact/suite.rb new file mode 100644 index 0000000000..cdfb757ddf --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/suite.rb @@ -0,0 +1,11 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::Suite < Hbc::Artifact::Moved + def self.artifact_english_name + "App Suite" + end + + def self.artifact_dirmethod + :appdir + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/symlinked.rb b/Library/Homebrew/cask/lib/hbc/artifact/symlinked.rb new file mode 100644 index 0000000000..749b0b98bd --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/symlinked.rb @@ -0,0 +1,65 @@ +require "hbc/artifact/relocated" + +class Hbc::Artifact::Symlinked < Hbc::Artifact::Relocated + def self.link_type_english_name + "Symlink" + end + + def self.english_description + "#{artifact_english_name} #{link_type_english_name}s" + end + + def self.islink?(path) + path.symlink? + end + + def link(artifact_spec) + load_specification artifact_spec + return unless preflight_checks(source, target) + ohai "#{self.class.link_type_english_name}ing #{self.class.artifact_english_name} '#{source.basename}' to '#{target}'" + create_filesystem_link(source, target) + end + + def unlink(artifact_spec) + load_specification artifact_spec + return unless self.class.islink?(target) + ohai "Removing #{self.class.artifact_english_name} #{self.class.link_type_english_name.downcase}: '#{target}'" + target.delete + end + + def install_phase + @cask.artifacts[self.class.artifact_dsl_key].each(&method(:link)) + end + + def uninstall_phase + @cask.artifacts[self.class.artifact_dsl_key].each(&method(:unlink)) + end + + def preflight_checks(source, target) + if target.exist? && !self.class.islink?(target) + ohai "It seems there is already #{self.class.artifact_english_article} #{self.class.artifact_english_name} at '#{target}'; not linking." + return false + end + unless source.exist? + raise Hbc::CaskError, "It seems the #{self.class.link_type_english_name.downcase} source is not there: '#{source}'" + end + true + end + + def create_filesystem_link(source, target) + Pathname.new(target).dirname.mkpath + @command.run!("/bin/ln", args: ["-hfs", "--", source, target]) + add_altname_metadata source, target.basename.to_s + end + + def summarize_artifact(artifact_spec) + load_specification artifact_spec + + return unless self.class.islink?(target) + + link_description = "#{Hbc::Utils::Tty.red.underline}Broken Link#{Hbc::Utils::Tty.reset}: " unless target.exist? + target_readlink_abv = " (#{target.readlink.abv})" if target.readlink.exist? + + "#{link_description}#{printable_target} -> #{target.readlink}#{target_readlink_abv}" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/uninstall.rb b/Library/Homebrew/cask/lib/hbc/artifact/uninstall.rb new file mode 100644 index 0000000000..12010aeb8b --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/uninstall.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/uninstall_base" + +class Hbc::Artifact::Uninstall < Hbc::Artifact::UninstallBase +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb b/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb new file mode 100644 index 0000000000..f92e09a893 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/uninstall_base.rb @@ -0,0 +1,249 @@ +require "pathname" + +require "hbc/artifact/base" + +class Hbc::Artifact::UninstallBase < Hbc::Artifact::Base + # TODO: 500 is also hardcoded in cask/pkg.rb, but much of + # that logic is probably in the wrong location + + PATH_ARG_SLICE_SIZE = 500 + + ORDERED_DIRECTIVES = [ + :early_script, + :launchctl, + :quit, + :signal, + :login_item, + :kext, + :script, + :pkgutil, + :delete, + :trash, + :rmdir, + ].freeze + + # TODO: these methods were consolidated here from separate + # sources and should be refactored for consistency + + def self.expand_path_strings(path_strings) + path_strings.map { |path_string| + path_string.start_with?("~") ? Pathname.new(path_string).expand_path : Pathname.new(path_string) + } + end + + def self.remove_relative_path_strings(action, path_strings) + relative = path_strings.map { |path_string| + path_string if %r{/\.\.(?:/|\Z)}.match(path_string) || !%r{\A/}.match(path_string) + }.compact + relative.each do |path_string| + opoo "Skipping #{action} for relative path #{path_string}" + end + path_strings - relative + end + + def self.remove_undeletable_path_strings(action, path_strings) + undeletable = path_strings.map { |path_string| + path_string if MacOS.undeletable?(Pathname.new(path_string)) + }.compact + undeletable.each do |path_string| + opoo "Skipping #{action} for undeletable path #{path_string}" + end + path_strings - undeletable + end + + def install_phase + odebug "Nothing to do. The uninstall artifact has no install phase." + end + + def uninstall_phase + dispatch_uninstall_directives + end + + def dispatch_uninstall_directives(expand_tilde = true) + directives_set = @cask.artifacts[stanza] + ohai "Running #{stanza} process for #{@cask}; your password may be necessary" + + directives_set.each do |directives| + warn_for_unknown_directives(directives) + end + + ORDERED_DIRECTIVES.each do |directive_sym| + directives_set.select { |h| h.key?(directive_sym) }.each do |directives| + args = [directives] + args << expand_tilde if [:delete, :trash, :rmdir].include?(directive_sym) + send("uninstall_#{directive_sym}", *args) + end + end + end + + private + + def stanza + self.class.artifact_dsl_key + end + + def warn_for_unknown_directives(directives) + unknown_keys = directives.keys - ORDERED_DIRECTIVES + return if unknown_keys.empty? + opoo %Q{Unknown arguments to #{stanza} -- #{unknown_keys.inspect}. Running "brew update; brew cleanup; brew cask cleanup" will likely fix it.} + end + + # Preserve prior functionality of script which runs first. Should rarely be needed. + # :early_script should not delete files, better defer that to :script. + # If Cask writers never need :early_script it may be removed in the future. + def uninstall_early_script(directives) + uninstall_script(directives, directive_name: :early_script) + end + + # :launchctl must come before :quit/:signal for cases where app would instantly re-launch + def uninstall_launchctl(directives) + Array(directives[:launchctl]).each do |service| + ohai "Removing launchctl service #{service}" + [false, true].each do |with_sudo| + plist_status = @command.run("/bin/launchctl", args: ["list", service], sudo: with_sudo, print_stderr: false).stdout + if plist_status =~ %r{^\{} + @command.run!("/bin/launchctl", args: ["remove", service], sudo: with_sudo) + sleep 1 + end + paths = ["/Library/LaunchAgents/#{service}.plist", + "/Library/LaunchDaemons/#{service}.plist"] + paths.each { |elt| elt.prepend(ENV["HOME"]) } unless with_sudo + paths = paths.map { |elt| Pathname(elt) }.select(&:exist?) + paths.each do |path| + @command.run!("/bin/rm", args: ["-f", "--", path], sudo: with_sudo) + end + # undocumented and untested: pass a path to uninstall :launchctl + next unless Pathname(service).exist? + @command.run!("/bin/launchctl", args: ["unload", "-w", "--", service], sudo: with_sudo) + @command.run!("/bin/rm", args: ["-f", "--", service], sudo: with_sudo) + sleep 1 + end + end + end + + # :quit/:signal must come before :kext so the kext will not be in use by a running process + def uninstall_quit(directives) + Array(directives[:quit]).each do |id| + ohai "Quitting application ID #{id}" + num_running = count_running_processes(id) + next unless num_running > 0 + @command.run!("/usr/bin/osascript", args: ["-e", %Q{tell application id "#{id}" to quit}], sudo: true) + sleep 3 + end + end + + # :signal should come after :quit so it can be used as a backup when :quit fails + def uninstall_signal(directives) + Array(directives[:signal]).flatten.each_slice(2) do |pair| + raise Hbc::CaskInvalidError.new(@cask, "Each #{stanza} :signal must have 2 elements.") unless pair.length == 2 + signal, id = pair + ohai "Signalling '#{signal}' to application ID '#{id}'" + pids = get_unix_pids(id) + next unless pids.any? + # Note that unlike :quit, signals are sent from the current user (not + # upgraded to the superuser). This is a todo item for the future, but + # there should be some additional thought/safety checks about that, as a + # misapplied "kill" by root could bring down the system. The fact that we + # learned the pid from AppleScript is already some degree of protection, + # though indirect. + odebug "Unix ids are #{pids.inspect} for processes with bundle identifier #{id}" + Process.kill(signal, *pids) + sleep 3 + end + end + + def count_running_processes(bundle_id) + @command.run!("/usr/bin/osascript", + args: ["-e", %Q{tell application "System Events" to count processes whose bundle identifier is "#{bundle_id}"}], + sudo: true).stdout.to_i + end + + def get_unix_pids(bundle_id) + pid_string = @command.run!("/usr/bin/osascript", + args: ["-e", %Q{tell application "System Events" to get the unix id of every process whose bundle identifier is "#{bundle_id}"}], + sudo: true).stdout.chomp + return [] unless pid_string =~ %r{\A\d+(?:\s*,\s*\d+)*\Z} # sanity check + pid_string.split(%r{\s*,\s*}).map(&:strip).map(&:to_i) + end + + def uninstall_login_item(directives) + Array(directives[:login_item]).each do |name| + ohai "Removing login item #{name}" + @command.run!("/usr/bin/osascript", + args: ["-e", %Q{tell application "System Events" to delete every login item whose name is "#{name}"}], + sudo: false) + sleep 1 + end + end + + # :kext should be unloaded before attempting to delete the relevant file + def uninstall_kext(directives) + Array(directives[:kext]).each do |kext| + ohai "Unloading kernel extension #{kext}" + is_loaded = @command.run!("/usr/sbin/kextstat", args: ["-l", "-b", kext], sudo: true).stdout + if is_loaded.length > 1 + @command.run!("/sbin/kextunload", args: ["-b", kext], sudo: true) + sleep 1 + end + end + end + + # :script must come before :pkgutil, :delete, or :trash so that the script file is not already deleted + def uninstall_script(directives, directive_name: :script) + executable, script_arguments = self.class.read_script_arguments(directives, + "uninstall", + { must_succeed: true, sudo: true }, + { print_stdout: true }, + directive_name) + ohai "Running uninstall script #{executable}" + raise Hbc::CaskInvalidError.new(@cask, "#{stanza} :#{directive_name} without :executable.") if executable.nil? + executable_path = @cask.staged_path.join(executable) + @command.run("/bin/chmod", args: ["--", "+x", executable_path]) if File.exist?(executable_path) + @command.run(executable_path, script_arguments) + sleep 1 + end + + def uninstall_pkgutil(directives) + ohai "Removing files from pkgutil Bill-of-Materials" + Array(directives[:pkgutil]).each do |regexp| + pkgs = Hbc::Pkg.all_matching(regexp, @command) + pkgs.each(&:uninstall) + end + end + + def uninstall_delete(directives, expand_tilde = true) + Array(directives[:delete]).concat(Array(directives[:trash])).flatten.each_slice(PATH_ARG_SLICE_SIZE) do |path_slice| + ohai "Removing files: #{path_slice.utf8_inspect}" + path_slice = self.class.expand_path_strings(path_slice) if expand_tilde + path_slice = self.class.remove_relative_path_strings(:delete, path_slice) + path_slice = self.class.remove_undeletable_path_strings(:delete, path_slice) + @command.run!("/bin/rm", args: path_slice.unshift("-rf", "--"), sudo: true) + end + end + + # :trash functionality is stubbed as a synonym for :delete + # TODO: make :trash work differently, moving files to the Trash + def uninstall_trash(directives, expand_tilde = true) + uninstall_delete(directives, expand_tilde) + end + + def uninstall_rmdir(directives, expand_tilde = true) + Array(directives[:rmdir]).flatten.each do |directory| + directory = self.class.expand_path_strings([directory]).first if expand_tilde + directory = self.class.remove_relative_path_strings(:rmdir, [directory]).first + directory = self.class.remove_undeletable_path_strings(:rmdir, [directory]).first + next if directory.to_s.empty? + ohai "Removing directory if empty: #{directory.to_s.utf8_inspect}" + directory = Pathname.new(directory) + next unless directory.exist? + @command.run!("/bin/rm", + args: ["-f", "--", directory.join(".DS_Store")], + sudo: true, + print_stderr: false) + @command.run("/bin/rmdir", + args: ["--", directory], + sudo: true, + print_stderr: false) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/vst3_plugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/vst3_plugin.rb new file mode 100644 index 0000000000..2438844357 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/vst3_plugin.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::Vst3Plugin < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/vst_plugin.rb b/Library/Homebrew/cask/lib/hbc/artifact/vst_plugin.rb new file mode 100644 index 0000000000..8d05464805 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/vst_plugin.rb @@ -0,0 +1,4 @@ +require "hbc/artifact/moved" + +class Hbc::Artifact::VstPlugin < Hbc::Artifact::Moved +end diff --git a/Library/Homebrew/cask/lib/hbc/artifact/zap.rb b/Library/Homebrew/cask/lib/hbc/artifact/zap.rb new file mode 100644 index 0000000000..8bd8da63bb --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/artifact/zap.rb @@ -0,0 +1,16 @@ +require "hbc/artifact/uninstall_base" + +class Hbc::Artifact::Zap < Hbc::Artifact::UninstallBase + def install_phase + odebug "Nothing to do. The zap artifact has no install phase." + end + + def uninstall_phase + odebug "Nothing to do. The zap artifact has no uninstall phase." + end + + def zap_phase + expand_tilde = true + dispatch_uninstall_directives(expand_tilde) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/audit.rb b/Library/Homebrew/cask/lib/hbc/audit.rb new file mode 100644 index 0000000000..98f09ffa43 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/audit.rb @@ -0,0 +1,216 @@ +require "hbc/checkable" +require "hbc/download" +require "digest" + +class Hbc::Audit + include Hbc::Checkable + + attr_reader :cask, :download + + def initialize(cask, download: false, check_token_conflicts: false, command: Hbc::SystemCommand) + @cask = cask + @download = download + @check_token_conflicts = check_token_conflicts + @command = command + end + + def check_token_conflicts? + @check_token_conflicts + end + + def run! + check_required_stanzas + check_version + check_sha256 + check_appcast + check_url + check_generic_artifacts + check_token_conflicts + check_download + self + rescue StandardError => e + odebug "#{e.message}\n#{e.backtrace.join("\n")}" + add_error "exception while auditing #{cask}: #{e.message}" + self + end + + def success? + !(errors? || warnings?) + end + + def summary_header + "audit for #{cask}" + end + + private + + def check_required_stanzas + odebug "Auditing required stanzas" + %i{version sha256 url homepage}.each do |sym| + add_error "a #{sym} stanza is required" unless cask.send(sym) + end + add_error "a license stanza is required (:unknown is OK)" unless cask.license + add_error "at least one name stanza is required" if cask.name.empty? + # TODO: specific DSL knowledge should not be spread around in various files like this + # TODO: nested_container should not still be a pseudo-artifact at this point + installable_artifacts = cask.artifacts.reject { |k| [:uninstall, :zap, :nested_container].include?(k) } + add_error "at least one activatable artifact stanza is required" if installable_artifacts.empty? + end + + def check_version + return unless cask.version + check_no_string_version_latest + end + + def check_no_string_version_latest + odebug "Verifying version :latest does not appear as a string ('latest')" + return unless cask.version.raw_version == "latest" + add_error "you should use version :latest instead of version 'latest'" + end + + def check_sha256 + return unless cask.sha256 + check_sha256_no_check_if_latest + check_sha256_actually_256 + check_sha256_invalid + end + + def check_sha256_no_check_if_latest + odebug "Verifying sha256 :no_check with version :latest" + return unless cask.version.latest? && cask.sha256 != :no_check + add_error "you should use sha256 :no_check when version is :latest" + end + + def check_sha256_actually_256(sha256: cask.sha256, stanza: "sha256") + odebug "Verifying #{stanza} string is a legal SHA-256 digest" + return unless sha256.is_a?(String) + return if sha256.length == 64 && sha256[%r{^[0-9a-f]+$}i] + add_error "#{stanza} string must be of 64 hexadecimal characters" + end + + def check_sha256_invalid(sha256: cask.sha256, stanza: "sha256") + odebug "Verifying #{stanza} is not a known invalid value" + empty_sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" + return unless sha256 == empty_sha256 + add_error "cannot use the sha256 for an empty string in #{stanza}: #{empty_sha256}" + end + + def check_appcast + return unless cask.appcast + odebug "Auditing appcast" + check_appcast_has_checkpoint + return unless cask.appcast.checkpoint + check_sha256_actually_256(sha256: cask.appcast.checkpoint, stanza: "appcast :checkpoint") + check_sha256_invalid(sha256: cask.appcast.checkpoint, stanza: "appcast :checkpoint") + return unless download + check_appcast_http_code + check_appcast_checkpoint_accuracy + end + + def check_appcast_has_checkpoint + odebug "Verifying appcast has :checkpoint key" + add_error "a checkpoint sha256 is required for appcast" unless cask.appcast.checkpoint + end + + def check_appcast_http_code + odebug "Verifying appcast returns 200 HTTP response code" + result = @command.run("/usr/bin/curl", args: ["--compressed", "--location", "--user-agent", Hbc::URL::FAKE_USER_AGENT, "--output", "/dev/null", "--write-out", "%{http_code}", cask.appcast], print_stderr: false) + if result.success? + http_code = result.stdout.chomp + add_warning "unexpected HTTP response code retrieving appcast: #{http_code}" unless http_code == "200" + else + add_warning "error retrieving appcast: #{result.stderr}" + end + end + + def check_appcast_checkpoint_accuracy + odebug "Verifying appcast checkpoint is accurate" + result = @command.run("/usr/bin/curl", args: ["--compressed", "--location", "--user-agent", Hbc::URL::FAKE_USER_AGENT, cask.appcast], print_stderr: false) + if result.success? + processed_appcast_text = result.stdout.gsub(%r{[^<]*}, "") + # This step is necessary to replicate running `sed` from the command line + processed_appcast_text << "\n" unless processed_appcast_text.end_with?("\n") + expected = cask.appcast.checkpoint + actual = Digest::SHA2.hexdigest(processed_appcast_text) + add_warning <<-EOS.undent unless expected == actual + appcast checkpoint mismatch + Expected: #{expected} + Actual: #{actual} + EOS + else + add_warning "error retrieving appcast: #{result.stderr}" + end + end + + def check_url + return unless cask.url + check_download_url_format + end + + def check_download_url_format + odebug "Auditing URL format" + if bad_sourceforge_url? + add_warning "SourceForge URL format incorrect. See https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#sourceforgeosdn-urls" + elsif bad_osdn_url? + add_warning "OSDN URL format incorrect. See https://github.com/caskroom/homebrew-cask/blob/master/doc/cask_language_reference/stanzas/url.md#sourceforgeosdn-urls" + end + end + + def bad_url_format?(regex, valid_formats_array) + return false unless cask.url.to_s =~ regex + valid_formats_array.none? { |format| cask.url.to_s =~ format } + end + + def bad_sourceforge_url? + bad_url_format?(%r{sourceforge}, + [ + %r{\Ahttps://sourceforge\.net/projects/[^/]+/files/latest/download\Z}, + %r{\Ahttps://downloads\.sourceforge\.net/(?!(project|sourceforge)\/)}, + # special cases: cannot find canonical format URL + %r{\Ahttps?://brushviewer\.sourceforge\.net/brushviewql\.zip\Z}, + %r{\Ahttps?://doublecommand\.sourceforge\.net/files/}, + %r{\Ahttps?://excalibur\.sourceforge\.net/get\.php\?id=}, + ]) + end + + def bad_osdn_url? + bad_url_format?(%r{osd}, [%r{\Ahttps?://([^/]+.)?dl\.osdn\.jp/}]) + end + + def check_generic_artifacts + cask.artifacts[:artifact].each do |source, target_hash| + unless target_hash.is_a?(Hash) && target_hash[:target] + add_error "target required for generic artifact #{source}" + next + end + add_error "target must be absolute path for generic artifact #{source}" unless Pathname.new(target_hash[:target]).absolute? + end + end + + def check_token_conflicts + return unless check_token_conflicts? + return unless core_formula_names.include?(cask.token) + add_warning "possible duplicate, cask token conflicts with Homebrew core formula: #{core_formula_url}" + end + + def core_tap + @core_tap ||= CoreTap.instance + end + + def core_formula_names + core_tap.formula_names + end + + def core_formula_url + "#{core_tap.default_remote}/blob/master/Formula/#{cask.token}.rb" + end + + def check_download + return unless download && cask.url + odebug "Auditing download" + downloaded_path = download.perform + Hbc::Verify.all(cask, downloaded_path) + rescue => e + add_error "download not possible: #{e.message}" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/auditor.rb b/Library/Homebrew/cask/lib/hbc/auditor.rb new file mode 100644 index 0000000000..89947c1aa3 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/auditor.rb @@ -0,0 +1,10 @@ +class Hbc::Auditor + def self.audit(cask, audit_download: false, check_token_conflicts: false) + download = audit_download && Hbc::Download.new(cask) + audit = Hbc::Audit.new(cask, download: download, + check_token_conflicts: check_token_conflicts) + audit.run! + puts audit.summary + audit.success? + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cache.rb b/Library/Homebrew/cask/lib/hbc/cache.rb new file mode 100644 index 0000000000..9fc5fe0f3c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cache.rb @@ -0,0 +1,34 @@ +module Hbc::Cache + module_function + + def ensure_cache_exists + return if Hbc.cache.exist? + odebug "Creating Cache at #{Hbc.cache}" + Hbc.cache.mkpath + end + + def migrate_legacy_cache + if Hbc.legacy_cache.exist? + ohai "Migrating cached files to #{Hbc.cache}..." + + Hbc.legacy_cache.children.select(&:symlink?).each do |symlink| + file = symlink.readlink + + new_name = file.basename + .sub(%r{\-((?:(\d|#{Hbc::DSL::Version::DIVIDER_REGEX})*\-\2*)*[^\-]+)$}x, + '--\1') + + renamed_file = Hbc.cache.join(new_name) + + if file.exist? + puts "#{file} -> #{renamed_file}" + FileUtils.mv(file, renamed_file) + end + + FileUtils.rm(symlink) + end + + FileUtils.remove_entry_secure(Hbc.legacy_cache) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cask.rb b/Library/Homebrew/cask/lib/hbc/cask.rb new file mode 100644 index 0000000000..fd13a6fe73 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cask.rb @@ -0,0 +1,114 @@ +require "forwardable" + +require "hbc/dsl" + +class Hbc::Cask + extend Forwardable + + attr_reader :token, :sourcefile_path + def initialize(token, sourcefile_path: nil, dsl: nil, &block) + @token = token + @sourcefile_path = sourcefile_path + @dsl = dsl || Hbc::DSL.new(@token) + @dsl.instance_eval(&block) if block_given? + end + + Hbc::DSL::DSL_METHODS.each do |method_name| + define_method(method_name) { @dsl.send(method_name) } + end + + METADATA_SUBDIR = ".metadata".freeze + + def metadata_master_container_path + @metadata_master_container_path ||= caskroom_path.join(METADATA_SUBDIR) + end + + def metadata_versioned_container_path + cask_version = version ? version : :unknown + metadata_master_container_path.join(cask_version.to_s) + end + + def metadata_path(timestamp = :latest, create = false) + return nil unless metadata_versioned_container_path.respond_to?(:join) + if create && timestamp == :latest + raise Hbc::CaskError, "Cannot create metadata path when timestamp is :latest" + end + path = if timestamp == :latest + Pathname.glob(metadata_versioned_container_path.join("*")).sort.last + elsif timestamp == :now + Hbc::Utils.nowstamp_metadata_path(metadata_versioned_container_path) + else + metadata_versioned_container_path.join(timestamp) + end + if create + odebug "Creating metadata directory #{path}" + FileUtils.mkdir_p path + end + path + end + + def metadata_subdir(leaf, timestamp = :latest, create = false) + if create && timestamp == :latest + raise Hbc::CaskError, "Cannot create metadata subdir when timestamp is :latest" + end + unless leaf.respond_to?(:length) && !leaf.empty? + raise Hbc::CaskError, "Cannot create metadata subdir for empty leaf" + end + parent = metadata_path(timestamp, create) + return nil unless parent.respond_to?(:join) + subdir = parent.join(leaf) + if create + odebug "Creating metadata subdirectory #{subdir}" + FileUtils.mkdir_p subdir + end + subdir + end + + def timestamped_versions + Pathname.glob(metadata_master_container_path.join("*", "*")) + .map { |p| p.relative_path_from(metadata_master_container_path) } + .sort_by(&:basename) # sort by timestamp + .map(&:split) + end + + def versions + timestamped_versions.map(&:first) + .reverse + .uniq + .reverse + end + + def installed? + !versions.empty? + end + + def to_s + @token + end + + def dumpcask + if Hbc.respond_to?(:debug) && Hbc.debug + odebug "Cask instance dumps in YAML:" + odebug "Cask instance toplevel:", to_yaml + [ + :name, + :homepage, + :url, + :appcast, + :version, + :license, + :sha256, + :artifacts, + :caveats, + :depends_on, + :conflicts_with, + :container, + :gpg, + :accessibility_access, + :auto_updates, + ].each do |method| + odebug "Cask instance method '#{method}':", send(method).to_yaml + end + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cask_dependencies.rb b/Library/Homebrew/cask/lib/hbc/cask_dependencies.rb new file mode 100644 index 0000000000..6cbfd05af2 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cask_dependencies.rb @@ -0,0 +1,33 @@ +require "hbc/topological_hash" + +class Hbc::CaskDependencies + attr_reader :cask, :graph, :sorted + + def initialize(cask) + @cask = cask + @graph = graph_dependencies + @sorted = sort + end + + def graph_dependencies + deps_in = ->(csk) { csk.depends_on ? csk.depends_on.cask || [] : [] } + walk = lambda { |acc, deps| + deps.each do |dep| + next if acc.key?(dep) + succs = deps_in.call Hbc.load(dep) + acc[dep] = succs + walk.call(acc, succs) + end + acc + } + + graphed = walk.call({}, @cask.depends_on.cask) + Hbc::TopologicalHash[graphed] + end + + def sort + @graph.tsort + rescue TSort::Cyclic + raise Hbc::CaskCyclicCaskDependencyError, @cask.token + end +end diff --git a/Library/Homebrew/cask/lib/hbc/caskroom.rb b/Library/Homebrew/cask/lib/hbc/caskroom.rb new file mode 100644 index 0000000000..cb471a125f --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/caskroom.rb @@ -0,0 +1,28 @@ +module Hbc::Caskroom + module_function + + def ensure_caskroom_exists + unless Hbc.caskroom.exist? + ohai "Creating Caskroom at #{Hbc.caskroom}" + + if Hbc.caskroom.parent.writable? + Hbc.caskroom.mkpath + else + ohai "We'll set permissions properly so we won't need sudo in the future" + toplevel_dir = Hbc.caskroom + toplevel_dir = toplevel_dir.parent until toplevel_dir.parent.root? + unless toplevel_dir.directory? + # If a toplevel dir such as '/opt' must be created, enforce standard permissions. + # sudo in system is rude. + system "/usr/bin/sudo", "--", "/bin/mkdir", "--", toplevel_dir + system "/usr/bin/sudo", "--", "/bin/chmod", "--", "0775", toplevel_dir + end + # sudo in system is rude. + system "/usr/bin/sudo", "--", "/bin/mkdir", "-p", "--", Hbc.caskroom + unless Hbc.caskroom.parent == toplevel_dir + system "/usr/bin/sudo", "--", "/usr/sbin/chown", "-R", "--", "#{Hbc::Utils.current_user}:staff", Hbc.caskroom.parent.to_s + end + end + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/caveats.rb b/Library/Homebrew/cask/lib/hbc/caveats.rb new file mode 100644 index 0000000000..04bbcf2186 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/caveats.rb @@ -0,0 +1,12 @@ +class Hbc::Caveats + def initialize(block) + @block = block + end + + def eval_and_print(cask) + dsl = Hbc::DSL::Caveats.new(cask) + retval = dsl.instance_eval(&@block) + return if retval.nil? + puts retval.to_s.sub(%r{[\r\n \t]*\Z}, "\n\n") + end +end diff --git a/Library/Homebrew/cask/lib/hbc/checkable.rb b/Library/Homebrew/cask/lib/hbc/checkable.rb new file mode 100644 index 0000000000..630a3f0630 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/checkable.rb @@ -0,0 +1,51 @@ +module Hbc::Checkable + def errors + Array(@errors) + end + + def warnings + Array(@warnings) + end + + def add_error(message) + @errors ||= [] + @errors << message + end + + def add_warning(message) + @warnings ||= [] + @warnings << message + end + + def errors? + Array(@errors).any? + end + + def warnings? + Array(@warnings).any? + end + + def result + if errors? + "#{Hbc::Utils::Tty.red.underline}failed#{Hbc::Utils::Tty.reset}" + elsif warnings? + "#{Hbc::Utils::Tty.yellow.underline}warning#{Hbc::Utils::Tty.reset}" + else + "#{Hbc::Utils::Tty.green}passed#{Hbc::Utils::Tty.reset}" + end + end + + def summary + summary = ["#{summary_header}: #{result}"] + + errors.each do |error| + summary << " #{Hbc::Utils::Tty.red}-#{Hbc::Utils::Tty.reset} #{error}" + end + + warnings.each do |warning| + summary << " #{Hbc::Utils::Tty.yellow}-#{Hbc::Utils::Tty.reset} #{warning}" + end + + summary.join("\n") + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli.rb b/Library/Homebrew/cask/lib/hbc/cli.rb new file mode 100644 index 0000000000..be40ce11b5 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli.rb @@ -0,0 +1,274 @@ +class Hbc::CLI; end + +require "optparse" +require "shellwords" + +require "hbc/cli/base" +require "hbc/cli/audit" +require "hbc/cli/cat" +require "hbc/cli/cleanup" +require "hbc/cli/create" +require "hbc/cli/doctor" +require "hbc/cli/edit" +require "hbc/cli/fetch" +require "hbc/cli/home" +require "hbc/cli/info" +require "hbc/cli/install" +require "hbc/cli/list" +require "hbc/cli/search" +require "hbc/cli/style" +require "hbc/cli/uninstall" +require "hbc/cli/update" +require "hbc/cli/zap" + +require "hbc/cli/internal_use_base" +require "hbc/cli/internal_audit_modified_casks" +require "hbc/cli/internal_checkurl" +require "hbc/cli/internal_dump" +require "hbc/cli/internal_help" +require "hbc/cli/internal_stanza" + +class Hbc::CLI + ALIASES = { + "ls" => "list", + "homepage" => "home", + "-S" => "search", # verb starting with "-" is questionable + "up" => "update", + "instal" => "install", # gem does the same + "rm" => "uninstall", + "remove" => "uninstall", + "abv" => "info", + "dr" => "doctor", + # aliases from Homebrew that we don't (yet) support + # 'ln' => 'link', + # 'configure' => 'diy', + # '--repo' => '--repository', + # 'environment' => '--env', + # '-c1' => '--config', + }.freeze + + OPTIONS = { + "--caskroom=" => :caskroom=, + "--appdir=" => :appdir=, + "--colorpickerdir=" => :colorpickerdir=, + "--prefpanedir=" => :prefpanedir=, + "--qlplugindir=" => :qlplugindir=, + "--fontdir=" => :fontdir=, + "--servicedir=" => :servicedir=, + "--input_methoddir=" => :input_methoddir=, + "--internet_plugindir=" => :internet_plugindir=, + "--audio_unit_plugindir=" => :audio_unit_plugindir=, + "--vst_plugindir=" => :vst_plugindir=, + "--vst3_plugindir=" => :vst3_plugindir=, + "--screen_saverdir=" => :screen_saverdir=, + }.freeze + + FLAGS = { + "--no-binaries" => :no_binaries=, + "--debug" => :debug=, + "--verbose" => :verbose=, + "--outdated" => :cleanup_outdated=, + "--help" => :help=, + }.freeze + + def self.command_classes + @command_classes ||= Hbc::CLI.constants + .map(&Hbc::CLI.method(:const_get)) + .select { |sym| sym.respond_to?(:run) } + end + + def self.commands + @commands ||= command_classes.map(&:command_name) + end + + def self.lookup_command(command_string) + @lookup ||= Hash[commands.zip(command_classes)] + command_string = ALIASES.fetch(command_string, command_string) + @lookup.fetch(command_string, command_string) + end + + # modified from Homebrew + def self.require?(path) + require path + true # OK if already loaded + rescue LoadError => e + # HACK: :( because we should raise on syntax errors + # but not if the file doesn't exist. + # TODO: make robust! + raise unless e.to_s.include? path + end + + def self.should_init?(command) + (command.is_a? Class) && (command < Hbc::CLI::Base) && command.needs_init? + end + + def self.run_command(command, *rest) + if command.respond_to?(:run) + # usual case: built-in command verb + command.run(*rest) + elsif require? Hbc::Utils.which("brewcask-#{command}.rb").to_s + # external command as Ruby library on PATH, Homebrew-style + elsif command.to_s.include?("/") && require?(command.to_s) + # external command as Ruby library with literal path, useful + # for development and troubleshooting + sym = Pathname.new(command.to_s).basename(".rb").to_s.capitalize + klass = begin + Hbc::CLI.const_get(sym) + rescue NameError + nil + end + if klass.respond_to?(:run) + # invoke "run" on a Ruby library which follows our coding conventions + klass.run(*rest) + else + # other Ruby libraries must do everything via "require" + end + elsif Hbc::Utils.which "brewcask-#{command}" + # arbitrary external executable on PATH, Homebrew-style + exec "brewcask-#{command}", *ARGV[1..-1] + elsif Pathname.new(command.to_s).executable? && + command.to_s.include?("/") && + !command.to_s.match(%r{\.rb$}) + # arbitrary external executable with literal path, useful + # for development and troubleshooting + exec command, *ARGV[1..-1] + else + # failure + Hbc::CLI::NullCommand.new(command).run + end + end + + def self.process(arguments) + command_string, *rest = *arguments + rest = process_options(rest) + command = Hbc.help ? "help" : lookup_command(command_string) + Hbc.init if should_init?(command) + run_command(command, *rest) + rescue Hbc::CaskError, Hbc::CaskSha256MismatchError => e + msg = e.message + msg << e.backtrace.join("\n") if Hbc.debug + onoe msg + exit 1 + rescue StandardError, ScriptError, NoMemoryError => e + msg = e.message + msg << Hbc::Utils.error_message_with_suggestions + msg << e.backtrace.join("\n") + onoe msg + exit 1 + end + + def self.nice_listing(cask_list) + cask_taps = {} + cask_list.each do |c| + user, repo, token = c.split "/" + repo.sub!(%r{^homebrew-}i, "") + cask_taps[token] ||= [] + cask_taps[token].push "#{user}/#{repo}" + end + list = [] + cask_taps.each do |token, taps| + if taps.length == 1 + list.push token + else + taps.each { |r| list.push [r, token].join "/" } + end + end + list.sort + end + + def self.parser + # If you modify these arguments, please update USAGE.md + @parser ||= OptionParser.new do |opts| + OPTIONS.each do |option, method| + opts.on("#{option}" "PATH", Pathname) do |path| + Hbc.public_send(method, path) + end + end + + opts.on("--binarydir=PATH") do + opoo <<-EOF.undent + Option --binarydir is obsolete! + Homebrew-Cask now uses the same location as your Homebrew installation for executable links. + EOF + end + + FLAGS.each do |flag, method| + opts.on(flag) do + Hbc.public_send(method, true) + end + end + + opts.on("--version") do + raise OptionParser::InvalidOption # override default handling of --version + end + end + end + + def self.process_options(args) + all_args = Shellwords.shellsplit(ENV["HOMEBREW_CASK_OPTS"] || "") + args + remaining = [] + until all_args.empty? + begin + head = all_args.shift + remaining.concat(parser.parse([head])) + rescue OptionParser::InvalidOption + remaining << head + retry + rescue OptionParser::MissingArgument + raise Hbc::CaskError, "The option '#{head}' requires an argument" + rescue OptionParser::AmbiguousOption + raise Hbc::CaskError, "There is more than one possible option that starts with '#{head}'" + end + end + + # for compat with Homebrew, not certain if this is desirable + Hbc.verbose = true if !ENV["VERBOSE"].nil? || !ENV["HOMEBREW_VERBOSE"].nil? + + remaining + end + + class NullCommand + def initialize(attempted_verb) + @attempted_verb = attempted_verb + end + + def run(*args) + if args.include?("--version") || @attempted_verb == "--version" + puts Hbc.full_version + else + purpose + usage + unless @attempted_verb.to_s.strip.empty? || @attempted_verb == "help" + raise Hbc::CaskError, "Unknown command: #{@attempted_verb}" + end + end + end + + def purpose + puts <<-PURPOSE.undent + brew-cask provides a friendly homebrew-style CLI workflow for the + administration of macOS applications distributed as binaries. + + PURPOSE + end + + def usage + max_command_len = Hbc::CLI.commands.map(&:length).max + + puts "Commands:\n\n" + Hbc::CLI.command_classes.each do |klass| + next unless klass.visible + puts " #{klass.command_name.ljust(max_command_len)} #{_help_for(klass)}" + end + puts %Q{\nSee also "man brew-cask"} + end + + def help + "" + end + + def _help_for(klass) + klass.respond_to?(:help) ? klass.help : nil + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/audit.rb b/Library/Homebrew/cask/lib/hbc/cli/audit.rb new file mode 100644 index 0000000000..289547b44c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/audit.rb @@ -0,0 +1,52 @@ +class Hbc::CLI::Audit < Hbc::CLI::Base + def self.help + "verifies installability of Casks" + end + + def self.run(*args) + failed_casks = new(args, Hbc::Auditor).run + return if failed_casks.empty? + raise Hbc::CaskError, "audit failed for casks: #{failed_casks.join(' ')}" + end + + def initialize(args, auditor) + @args = args + @auditor = auditor + end + + def run + casks_to_audit.each_with_object([]) do |cask, failed| + failed << cask unless audit(cask) + end + end + + def audit(cask) + odebug "Auditing Cask #{cask}" + @auditor.audit(cask, audit_download: audit_download?, + check_token_conflicts: check_token_conflicts?) + end + + def audit_download? + @args.include?("--download") + end + + def check_token_conflicts? + @args.include?("--token-conflicts") + end + + def casks_to_audit + if cask_tokens.empty? + Hbc.all + else + cask_tokens.map { |token| Hbc.load(token) } + end + end + + def cask_tokens + @cask_tokens ||= self.class.cask_tokens_from(@args) + end + + def self.needs_init? + true + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/base.rb b/Library/Homebrew/cask/lib/hbc/cli/base.rb new file mode 100644 index 0000000000..af03969af7 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/base.rb @@ -0,0 +1,21 @@ +class Hbc::CLI::Base + def self.command_name + @command_name ||= name.sub(%r{^.*:}, "").gsub(%r{(.)([A-Z])}, '\1_\2').downcase + end + + def self.visible + true + end + + def self.cask_tokens_from(args) + args.reject { |a| a.empty? || a.chars.first == "-" } + end + + def self.help + "No help available for the #{command_name} command" + end + + def self.needs_init? + false + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/cat.rb b/Library/Homebrew/cask/lib/hbc/cli/cat.rb new file mode 100644 index 0000000000..d6d545c3b5 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/cat.rb @@ -0,0 +1,15 @@ +class Hbc::CLI::Cat < Hbc::CLI::Base + def self.run(*args) + cask_tokens = cask_tokens_from(args) + raise Hbc::CaskUnspecifiedError if cask_tokens.empty? + # only respects the first argument + cask_token = cask_tokens.first.sub(%r{\.rb$}i, "") + cask_path = Hbc.path(cask_token) + raise Hbc::CaskUnavailableError, cask_token.to_s unless cask_path.exist? + puts File.open(cask_path, &:read) + end + + def self.help + "dump raw source of the given Cask to the standard output" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/cleanup.rb b/Library/Homebrew/cask/lib/hbc/cli/cleanup.rb new file mode 100644 index 0000000000..b098a243d0 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/cleanup.rb @@ -0,0 +1,88 @@ +class Hbc::CLI::Cleanup < Hbc::CLI::Base + OUTDATED_DAYS = 10 + OUTDATED_TIMESTAMP = Time.now - (60 * 60 * 24 * OUTDATED_DAYS) + + def self.help + "cleans up cached downloads and tracker symlinks" + end + + def self.needs_init? + true + end + + def self.run(*_ignored) + default.cleanup! + end + + def self.default + @default ||= new(Hbc.cache, Hbc.cleanup_outdated) + end + + attr_reader :cache_location, :outdated_only + def initialize(cache_location, outdated_only) + @cache_location = Pathname.new(cache_location) + @outdated_only = outdated_only + end + + def cleanup! + remove_all_cache_files + end + + def cache_files + return [] unless cache_location.exist? + cache_location.children + .map(&method(:Pathname)) + .reject(&method(:outdated?)) + end + + def outdated?(file) + outdated_only && file && file.stat.mtime > OUTDATED_TIMESTAMP + end + + def incomplete?(file) + file.extname == ".incomplete" + end + + def cache_incompletes + cache_files.select(&method(:incomplete?)) + end + + def cache_completes + cache_files.reject(&method(:incomplete?)) + end + + def disk_cleanup_size + Hbc::Utils.size_in_bytes(cache_files) + end + + def remove_all_cache_files + message = "Removing cached downloads" + message.concat " older than #{OUTDATED_DAYS} days old" if outdated_only + ohai message + delete_paths(cache_files) + end + + def delete_paths(paths) + cleanup_size = 0 + processed_files = 0 + paths.each do |item| + next unless item.exist? + processed_files += 1 + if Hbc::Utils.file_locked?(item) + puts "skipping: #{item} is locked" + next + end + puts item + item_size = File.size?(item) + cleanup_size += item_size unless item_size.nil? + item.unlink + end + + if processed_files.zero? + puts "Nothing to do" + else + disk_space = disk_usage_readable(cleanup_size) + ohai "This operation has freed approximately #{disk_space} of disk space." + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/create.rb b/Library/Homebrew/cask/lib/hbc/cli/create.rb new file mode 100644 index 0000000000..3c1ac76ed0 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/create.rb @@ -0,0 +1,37 @@ +class Hbc::CLI::Create < Hbc::CLI::Base + def self.run(*args) + cask_tokens = cask_tokens_from(args) + raise Hbc::CaskUnspecifiedError if cask_tokens.empty? + cask_token = cask_tokens.first.sub(%r{\.rb$}i, "") + cask_path = Hbc.path(cask_token) + odebug "Creating Cask #{cask_token}" + + raise Hbc::CaskAlreadyCreatedError, cask_token if cask_path.exist? + + File.open(cask_path, "w") do |f| + f.write template(cask_token) + end + + exec_editor cask_path + end + + def self.template(cask_token) + <<-EOS.undent + cask '#{cask_token}' do + version '' + sha256 '' + + url 'https://' + name '' + homepage '' + license :unknown # TODO: change license and remove this comment; ':unknown' is a machine-generated placeholder + + app '' + end + EOS + end + + def self.help + "creates the given Cask and opens it in an editor" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/doctor.rb b/Library/Homebrew/cask/lib/hbc/cli/doctor.rb new file mode 100644 index 0000000000..7ee861508d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/doctor.rb @@ -0,0 +1,213 @@ +class Hbc::CLI::Doctor < Hbc::CLI::Base + def self.run + ohai "macOS Release:", render_with_none_as_error(MacOS.full_version) + ohai "Hardware Architecture:", render_with_none_as_error("#{Hardware::CPU.type}-#{Hardware::CPU.bits}") + ohai "Ruby Version:", render_with_none_as_error("#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL}") + ohai "Ruby Path:", render_with_none_as_error(RbConfig.ruby) + # TODO: consider removing most Homebrew constants from doctor output + ohai "Homebrew Version:", render_with_none_as_error(homebrew_version) + ohai "Homebrew Executable Path:", render_with_none_as_error(Hbc.homebrew_executable) + ohai "Homebrew Cellar Path:", render_with_none_as_error(homebrew_cellar) + ohai "Homebrew Repository Path:", render_with_none_as_error(homebrew_repository) + ohai "Homebrew Origin:", render_with_none_as_error(homebrew_origin) + ohai "Homebrew-Cask Version:", render_with_none_as_error(Hbc.full_version) + ohai "Homebrew-Cask Install Location:", render_install_location + ohai "Homebrew-Cask Staging Location:", render_staging_location(Hbc.caskroom) + ohai "Homebrew-Cask Cached Downloads:", render_cached_downloads + ohai "Homebrew-Cask Default Tap Path:", render_tap_paths(Hbc.default_tap.path) + ohai "Homebrew-Cask Alternate Cask Taps:", render_tap_paths(alt_taps) + ohai "Homebrew-Cask Default Tap Cask Count:", render_with_none_as_error(default_cask_count) + ohai "Contents of $LOAD_PATH:", render_load_path($LOAD_PATH) + ohai "Contents of $RUBYLIB Environment Variable:", render_env_var("RUBYLIB") + ohai "Contents of $RUBYOPT Environment Variable:", render_env_var("RUBYOPT") + ohai "Contents of $RUBYPATH Environment Variable:", render_env_var("RUBYPATH") + ohai "Contents of $RBENV_VERSION Environment Variable:", render_env_var("RBENV_VERSION") + ohai "Contents of $CHRUBY_VERSION Environment Variable:", render_env_var("CHRUBY_VERSION") + ohai "Contents of $GEM_HOME Environment Variable:", render_env_var("GEM_HOME") + ohai "Contents of $GEM_PATH Environment Variable:", render_env_var("GEM_PATH") + ohai "Contents of $BUNDLE_PATH Environment Variable:", render_env_var("BUNDLE_PATH") + ohai "Contents of $PATH Environment Variable:", render_env_var("PATH") + ohai "Contents of $SHELL Environment Variable:", render_env_var("SHELL") + ohai "Contents of Locale Environment Variables:", render_with_none(locale_variables) + ohai "Running As Privileged User:", render_with_none_as_error(privileged_uid) + end + + def self.alt_taps + Tap.select { |t| t.cask_dir.directory? && t != Hbc.default_tap } + .map(&:path) + end + + def self.default_cask_count + default_cask_count = notfound_string + begin + default_cask_count = Hbc.default_tap.cask_dir.children.count(&:file?) + rescue StandardError + default_cask_count = "0 #{error_string "Error reading #{Hbc.default_tap.path}"}" + end + default_cask_count + end + + def self.homebrew_origin + homebrew_origin = notfound_string + begin + Dir.chdir(homebrew_repository) do + homebrew_origin = Hbc::SystemCommand.run("/usr/bin/git", + args: %w[config --get remote.origin.url], + print_stderr: false).stdout.strip + end + if homebrew_origin !~ %r{\S} + homebrew_origin = "#{none_string} #{error_string}" + elsif homebrew_origin !~ %r{(mxcl|Homebrew)/(home)?brew(\.git)?\Z} + homebrew_origin.concat " #{error_string 'warning: nonstandard origin'}" + end + rescue StandardError + homebrew_origin = error_string "Not Found - Error running git" + end + homebrew_origin + end + + def self.homebrew_repository + homebrew_constants("repository") + end + + def self.homebrew_cellar + homebrew_constants("cellar") + end + + def self.homebrew_version + homebrew_constants("version") + end + + def self.homebrew_taps + @homebrew_taps ||= if homebrew_repository.respond_to?(:join) + homebrew_repository.join("Library", "Taps") + end + end + + def self.homebrew_constants(name) + @homebrew_constants ||= {} + return @homebrew_constants[name] if @homebrew_constants.key?(name) + @homebrew_constants[name] = notfound_string + begin + @homebrew_constants[name] = Hbc::SystemCommand.run!(Hbc.homebrew_executable, + args: ["--#{name}"], + print_stderr: false) + .stdout + .strip + if @homebrew_constants[name] !~ %r{\S} + @homebrew_constants[name] = "#{none_string} #{error_string}" + end + path = Pathname.new(@homebrew_constants[name]) + @homebrew_constants[name] = path if path.exist? + rescue StandardError + @homebrew_constants[name] = error_string "Not Found - Error running brew" + end + @homebrew_constants[name] + end + + def self.locale_variables + ENV.keys.grep(%r{^(?:LC_\S+|LANG|LANGUAGE)\Z}).collect { |v| %Q{#{v}="#{ENV[v]}"} }.sort.join("\n") + end + + def self.privileged_uid + Process.euid == 0 ? "Yes #{error_string 'warning: not recommended'}" : "No" + rescue StandardError + notfound_string + end + + def self.none_string + "" + end + + def self.legacy_tap_pattern + %r{phinze} + end + + def self.notfound_string + "#{Hbc::Utils::Tty.red.underline}Not Found - Unknown Error#{Hbc::Utils::Tty.reset}" + end + + def self.error_string(string = "Error") + "#{Hbc::Utils::Tty.red.underline}(#{string})#{Hbc::Utils::Tty.reset}" + end + + def self.render_with_none(string) + return string if !string.nil? && string.respond_to?(:to_s) && !string.to_s.empty? + none_string + end + + def self.render_with_none_as_error(string) + return string if !string.nil? && string.respond_to?(:to_s) && !string.to_s.empty? + "#{none_string} #{error_string}" + end + + def self.render_tap_paths(paths) + paths = [paths] unless paths.respond_to?(:each) + paths.collect do |dir| + if dir.nil? || dir.to_s.empty? + none_string + elsif dir.to_s.match(legacy_tap_pattern) + dir.to_s.concat(" #{error_string 'Warning: legacy tap path'}") + else + dir.to_s + end + end + end + + def self.render_env_var(var) + if ENV.key?(var) + %Q{#{var}="#{ENV[var]}"} + else + none_string + end + end + + # This could be done by calling into Homebrew, but the situation + # where "doctor" is needed is precisely the situation where such + # things are less dependable. + def self.render_install_location + locations = Dir.glob(Pathname.new(homebrew_cellar).join("brew-cask", "*")).reverse + if locations.empty? + none_string + else + locations.collect do |l| + "#{l} #{error_string 'error: legacy install. Run "brew uninstall --force brew-cask".'}" + end + end + end + + def self.render_staging_location(path) + path = Pathname.new(path) + if !path.exist? + "#{path} #{error_string 'error: path does not exist'}}" + elsif !path.writable? + "#{path} #{error_string 'error: not writable by current user'}" + else + path + end + end + + def self.render_load_path(paths) + return "#{none_string} #{error_string}" if paths.nil? || paths.empty? + copy = Array.new(paths) + unless Hbc::Utils.file_is_descendant(copy[0], homebrew_taps) + copy[0] = "#{copy[0]} #{error_string 'error: should be descendant of Homebrew taps directory'}" + end + copy + end + + def self.render_cached_downloads + cleanup = Hbc::CLI::Cleanup.default + files = cleanup.cache_files + count = files.count + size = cleanup.disk_cleanup_size + size_msg = "#{number_readable(count)} files, #{disk_usage_readable(size)}" + warn_msg = error_string('warning: run "brew cask cleanup"') + size_msg << " #{warn_msg}" if count > 0 + [Hbc.cache, size_msg] + end + + def self.help + "checks for configuration issues" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/edit.rb b/Library/Homebrew/cask/lib/hbc/cli/edit.rb new file mode 100644 index 0000000000..b2d4a91564 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/edit.rb @@ -0,0 +1,18 @@ +class Hbc::CLI::Edit < Hbc::CLI::Base + def self.run(*args) + cask_tokens = cask_tokens_from(args) + raise Hbc::CaskUnspecifiedError if cask_tokens.empty? + # only respects the first argument + cask_token = cask_tokens.first.sub(%r{\.rb$}i, "") + cask_path = Hbc.path(cask_token) + odebug "Opening editor for Cask #{cask_token}" + unless cask_path.exist? + raise Hbc::CaskUnavailableError, %Q{#{cask_token}, run "brew cask create #{cask_token}" to create a new Cask} + end + exec_editor cask_path + end + + def self.help + "edits the given Cask" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/fetch.rb b/Library/Homebrew/cask/lib/hbc/cli/fetch.rb new file mode 100644 index 0000000000..647f2af2c1 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/fetch.rb @@ -0,0 +1,19 @@ +class Hbc::CLI::Fetch < Hbc::CLI::Base + def self.run(*args) + cask_tokens = cask_tokens_from(args) + raise Hbc::CaskUnspecifiedError if cask_tokens.empty? + force = args.include? "--force" + + cask_tokens.each do |cask_token| + ohai "Downloading external files for Cask #{cask_token}" + cask = Hbc.load(cask_token) + downloaded_path = Hbc::Download.new(cask, force: force).perform + Hbc::Verify.all(cask, downloaded_path) + ohai "Success! Downloaded to -> #{downloaded_path}" + end + end + + def self.help + "downloads remote application files to local cache" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/home.rb b/Library/Homebrew/cask/lib/hbc/cli/home.rb new file mode 100644 index 0000000000..9c8c0a0e43 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/home.rb @@ -0,0 +1,18 @@ +class Hbc::CLI::Home < Hbc::CLI::Base + def self.run(*cask_tokens) + if cask_tokens.empty? + odebug "Opening project homepage" + system "/usr/bin/open", "--", "http://caskroom.io/" + else + cask_tokens.each do |cask_token| + odebug "Opening homepage for Cask #{cask_token}" + cask = Hbc.load(cask_token) + system "/usr/bin/open", "--", cask.homepage + end + end + end + + def self.help + "opens the homepage of the given Cask" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/info.rb b/Library/Homebrew/cask/lib/hbc/cli/info.rb new file mode 100644 index 0000000000..dda4057056 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/info.rb @@ -0,0 +1,66 @@ +class Hbc::CLI::Info < Hbc::CLI::Base + def self.run(*args) + cask_tokens = cask_tokens_from(args) + raise Hbc::CaskUnspecifiedError if cask_tokens.empty? + cask_tokens.each do |cask_token| + odebug "Getting info for Cask #{cask_token}" + cask = Hbc.load(cask_token) + + info(cask) + end + end + + def self.help + "displays information about the given Cask" + end + + def self.info(cask) + puts "#{cask.token}: #{cask.version}" + puts formatted_url(cask.homepage) if cask.homepage + installation_info(cask) + puts "From: #{formatted_url(github_info(cask))}" if github_info(cask) + name_info(cask) + artifact_info(cask) + Hbc::Installer.print_caveats(cask) + end + + def self.formatted_url(url) + "#{Hbc::Utils::Tty.underline}#{url}#{Hbc::Utils::Tty.reset}" + end + + def self.installation_info(cask) + if cask.installed? + cask.versions.each do |version| + versioned_staged_path = cask.caskroom_path.join(version) + + puts versioned_staged_path.to_s + .concat(" (") + .concat(versioned_staged_path.exist? ? versioned_staged_path.abv : "#{Hbc::Utils::Tty.red}does not exist#{Hbc::Utils::Tty.reset}") + .concat(")") + end + else + puts "Not installed" + end + end + + def self.name_info(cask) + ohai cask.name.size > 1 ? "Names" : "Name" + puts cask.name.empty? ? "#{Hbc::Utils::Tty.red}None#{Hbc::Utils::Tty.reset}" : cask.name + end + + def self.github_info(cask) + user, repo, token = Hbc::QualifiedToken.parse(Hbc.all_tokens.detect { |t| t.split("/").last == cask.token }) + "#{Tap.fetch(user, repo).default_remote}/blob/master/Casks/#{token}.rb" + end + + def self.artifact_info(cask) + ohai "Artifacts" + Hbc::DSL::ORDINARY_ARTIFACT_TYPES.each do |type| + next if cask.artifacts[type].empty? + cask.artifacts[type].each do |artifact| + activatable_item = type == :stage_only ? "" : artifact.first + puts "#{activatable_item} (#{type})" + end + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/install.rb b/Library/Homebrew/cask/lib/hbc/cli/install.rb new file mode 100644 index 0000000000..43eab9f3d5 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/install.rb @@ -0,0 +1,60 @@ + +class Hbc::CLI::Install < Hbc::CLI::Base + def self.run(*args) + cask_tokens = cask_tokens_from(args) + raise Hbc::CaskUnspecifiedError if cask_tokens.empty? + force = args.include? "--force" + skip_cask_deps = args.include? "--skip-cask-deps" + require_sha = args.include? "--require-sha" + retval = install_casks cask_tokens, force, skip_cask_deps, require_sha + # retval is ternary: true/false/nil + + raise Hbc::CaskError, "nothing to install" if retval.nil? + raise Hbc::CaskError, "install incomplete" unless retval + end + + def self.install_casks(cask_tokens, force, skip_cask_deps, require_sha) + count = 0 + cask_tokens.each do |cask_token| + begin + cask = Hbc.load(cask_token) + Hbc::Installer.new(cask, + force: force, + skip_cask_deps: skip_cask_deps, + require_sha: require_sha).install + count += 1 + rescue Hbc::CaskAlreadyInstalledError => e + opoo e.message + count += 1 + rescue Hbc::CaskAutoUpdatesError => e + opoo e.message + count += 1 + rescue Hbc::CaskUnavailableError => e + warn_unavailable_with_suggestion cask_token, e + rescue Hbc::CaskNoShasumError => e + opoo e.message + count += 1 + end + end + count == 0 ? nil : count == cask_tokens.length + end + + def self.warn_unavailable_with_suggestion(cask_token, e) + exact_match, partial_matches = Hbc::CLI::Search.search(cask_token) + errmsg = e.message + if exact_match + errmsg.concat(". Did you mean:\n#{exact_match}") + elsif !partial_matches.empty? + errmsg.concat(". Did you mean one of:\n#{puts_columns(partial_matches.take(20))}\n") + end + onoe errmsg + end + + def self.help + "installs the given Cask" + end + + def self.needs_init? + true + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_audit_modified_casks.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_audit_modified_casks.rb new file mode 100644 index 0000000000..f05dbe8038 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/internal_audit_modified_casks.rb @@ -0,0 +1,135 @@ +class Hbc::CLI::InternalAuditModifiedCasks < Hbc::CLI::InternalUseBase + RELEVANT_STANZAS = %i{version sha256 url appcast}.freeze + + class << self + def needs_init? + true + end + + def run(*args) + commit_range = commit_range(args) + cleanup = args.any? { |a| a =~ %r{^-+c(leanup)?$}i } + new(commit_range, cleanup: cleanup).run + end + + def commit_range(args) + posargs = args.reject { |a| a.empty? || a.chars.first == "-" } + odie usage unless posargs.size == 1 + posargs.first + end + + def posargs(args) + args.reject { |a| a.empty? || a.chars.first == "-" } + end + + def usage + <<-EOS.undent + Usage: brew cask _audit_modified_casks [options...] + + Given a range of Git commits, find any Casks that were modified and run `brew + cask audit' on them. If the `url', `version', or `sha256' stanzas were modified, + run with the `--download' flag to verify the hash. + + Options: + -c, --cleanup + Remove all cached downloads. Use with care. + EOS + end + end + + def initialize(commit_range, cleanup: false) + @commit_range = commit_range + @cleanup = cleanup + end + + attr_reader :commit_range + + def cleanup? + @cleanup + end + + def run + at_exit do + cleanup + end + + Dir.chdir git_root do + modified_cask_files.zip(modified_casks).each do |cask_file, cask| + audit(cask, cask_file) + end + end + report_failures + end + + def git_root + @git_root ||= git(*%w[rev-parse --show-toplevel]) + end + + def modified_cask_files + @modified_cask_files ||= git_filter_cask_files("AM") + end + + def added_cask_files + @added_cask_files ||= git_filter_cask_files("A") + end + + def git_filter_cask_files(filter) + git("diff", "--name-only", "--diff-filter=#{filter}", commit_range, + "--", Pathname.new(git_root).join("Casks", "*.rb").to_s).split("\n") + end + + def modified_casks + return @modified_casks if defined? @modified_casks + @modified_casks = modified_cask_files.map { |f| Hbc.load(f) } + if @modified_casks.any? + num_modified = @modified_casks.size + ohai "#{num_modified} modified #{pluralize('cask', num_modified)}: " \ + "#{@modified_casks.join(' ')}" + end + @modified_casks + end + + def audit(cask, cask_file) + audit_download = audit_download?(cask, cask_file) + check_token_conflicts = added_cask_files.include?(cask_file) + success = Hbc::Auditor.audit(cask, audit_download: audit_download, + check_token_conflicts: check_token_conflicts) + failed_casks << cask unless success + end + + def failed_casks + @failed_casks ||= [] + end + + def audit_download?(cask, cask_file) + cask.sha256 != :no_check && relevant_stanza_modified?(cask_file) + end + + def relevant_stanza_modified?(cask_file) + out = git("diff", commit_range, "--", cask_file) + out =~ %r{^\+\s*(#{RELEVANT_STANZAS.join('|')})} + end + + def git(*args) + odebug ["git", *args].join(" ") + out, err, status = Open3.capture3("git", *args) + return out.chomp if status.success? + odie err.chomp + end + + def report_failures + return if failed_casks.empty? + num_failed = failed_casks.size + cask_pluralized = pluralize("cask", num_failed) + odie "audit failed for #{num_failed} #{cask_pluralized}: " \ + "#{failed_casks.join(' ')}" + end + + def pluralize(str, num) + num == 1 ? str : "#{str}s" + end + + def cleanup + Hbc::CLI::Cleanup.run if cleanup? + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_checkurl.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_checkurl.rb new file mode 100644 index 0000000000..d53f420e28 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/internal_checkurl.rb @@ -0,0 +1,15 @@ +class Hbc::CLI::InternalCheckurl < Hbc::CLI::InternalUseBase + def self.run(*args) + casks_to_check = args.empty? ? Hbc.all : args.map { |arg| Hbc.load(arg) } + casks_to_check.each do |cask| + odebug "Checking URL for Cask #{cask}" + checker = Hbc::UrlChecker.new(cask) + checker.run + puts checker.summary + end + end + + def self.help + "checks for bad Cask URLs" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_dump.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_dump.rb new file mode 100644 index 0000000000..d1cfe8d632 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/internal_dump.rb @@ -0,0 +1,30 @@ +class Hbc::CLI::InternalDump < Hbc::CLI::InternalUseBase + def self.run(*arguments) + cask_tokens = cask_tokens_from(arguments) + raise Hbc::CaskUnspecifiedError if cask_tokens.empty? + retval = dump_casks(*cask_tokens) + # retval is ternary: true/false/nil + + raise Hbc::CaskError, "nothing to dump" if retval.nil? + raise Hbc::CaskError, "dump incomplete" unless retval + end + + def self.dump_casks(*cask_tokens) + Hbc.debug = true # Yuck. At the moment this is the only way to make dumps visible + count = 0 + cask_tokens.each do |cask_token| + begin + cask = Hbc.load(cask_token) + count += 1 + cask.dumpcask + rescue StandardError => e + opoo "#{cask_token} was not found or would not load: #{e}" + end + end + count == 0 ? nil : count == cask_tokens.length + end + + def self.help + "Dump the given Cask in YAML format" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_help.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_help.rb new file mode 100644 index 0000000000..81d7ee6731 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/internal_help.rb @@ -0,0 +1,19 @@ +class Hbc::CLI::InternalHelp < Hbc::CLI::InternalUseBase + def self.run(*_ignored) + max_command_len = Hbc::CLI.commands.map(&:length).max + puts "Unstable Internal-use Commands:\n\n" + Hbc::CLI.command_classes.each do |klass| + next if klass.visible + puts " #{klass.command_name.ljust(max_command_len)} #{help_for(klass)}" + end + puts "\n" + end + + def self.help_for(klass) + klass.respond_to?(:help) ? klass.help : nil + end + + def self.help + "Print help strings for unstable internal-use commands" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_stanza.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_stanza.rb new file mode 100644 index 0000000000..651a9ae374 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/internal_stanza.rb @@ -0,0 +1,127 @@ +class Hbc::CLI::InternalStanza < Hbc::CLI::InternalUseBase + # Syntax + # + # brew cask _stanza [ --table | --yaml | --inspect | --quiet ] [ ... ] + # + # If no tokens are given, then data for all Casks is returned. + # + # The pseudo-stanza "artifacts" is available. + # + # On failure, a blank line is returned on the standard output. + # + # Examples + # + # brew cask _stanza appcast --table + # brew cask _stanza app --table alfred google-chrome adium voicemac logisim vagrant + # brew cask _stanza url --table alfred google-chrome adium voicemac logisim vagrant + # brew cask _stanza version --table alfred google-chrome adium voicemac logisim vagrant + # brew cask _stanza artifacts --table --inspect alfred google-chrome adium voicemac logisim vagrant + # brew cask _stanza artifacts --table --yaml alfred google-chrome adium voicemac logisim vagrant + # + + # TODO: this should be retrievable from Hbc::DSL + ARTIFACTS = Set.new [ + :app, + :suite, + :artifact, + :prefpane, + :qlplugin, + :font, + :service, + :colorpicker, + :binary, + :input_method, + :internet_plugin, + :audio_unit_plugin, + :vst_plugin, + :vst3_plugin, + :screen_saver, + :pkg, + :installer, + :stage_only, + :nested_container, + :uninstall, + :postflight, + :uninstall_postflight, + :preflight, + :uninstall_postflight, + ] + + def self.run(*arguments) + table = arguments.include? "--table" + quiet = arguments.include? "--quiet" + format = :to_yaml if arguments.include? "--yaml" + format = :inspect if arguments.include? "--inspect" + cask_tokens = arguments.reject { |arg| arg.chars.first == "-" } + stanza = cask_tokens.shift.to_sym + cask_tokens = Hbc.all_tokens if cask_tokens.empty? + + retval = print_stanzas(stanza, format, table, quiet, *cask_tokens) + + # retval is ternary: true/false/nil + if retval.nil? + exit 1 if quiet + raise Hbc::CaskError, "nothing to print" + elsif !retval + exit 1 if quiet + raise Hbc::CaskError, "print incomplete" + end + end + + def self.print_stanzas(stanza, format = nil, table = nil, quiet = nil, *cask_tokens) + count = 0 + if ARTIFACTS.include?(stanza) + artifact_name = stanza + stanza = :artifacts + end + + cask_tokens.each do |cask_token| + print "#{cask_token}\t" if table + + begin + cask = Hbc.load(cask_token) + rescue StandardError + opoo "Cask '#{cask_token}' was not found" unless quiet + puts "" + next + end + + unless cask.respond_to?(stanza) + opoo "no such stanza '#{stanza}' on Cask '#{cask_token}'" unless quiet + puts "" + next + end + + begin + value = cask.send(stanza) + rescue StandardError + opoo "failure calling '#{stanza}' on Cask '#{cask_token}'" unless quiet + puts "" + next + end + + if artifact_name && !value.key?(artifact_name) + opoo "no such stanza '#{artifact_name}' on Cask '#{cask_token}'" unless quiet + puts "" + next + end + + value = value.fetch(artifact_name).to_a.flatten if artifact_name + + if format + puts value.send(format) + elsif artifact_name || value.is_a?(Symbol) + puts value.inspect + else + puts value.to_s + end + + count += 1 + end + count == 0 ? nil : count == cask_tokens.length + end + + def self.help + "Extract and render a specific stanza for the given Casks" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/internal_use_base.rb b/Library/Homebrew/cask/lib/hbc/cli/internal_use_base.rb new file mode 100644 index 0000000000..6a4359ea17 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/internal_use_base.rb @@ -0,0 +1,9 @@ +class Hbc::CLI::InternalUseBase < Hbc::CLI::Base + def self.command_name + super.sub(%r{^internal_}i, "_") + end + + def self.visible + false + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/list.rb b/Library/Homebrew/cask/lib/hbc/cli/list.rb new file mode 100644 index 0000000000..ce507a827a --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/list.rb @@ -0,0 +1,82 @@ +class Hbc::CLI::List < Hbc::CLI::Base + def self.run(*arguments) + @options = {} + @options[:one] = true if arguments.delete("-1") + @options[:versions] = true if arguments.delete("--versions") + + if arguments.delete("-l") + @options[:one] = true + opoo "Option -l is obsolete! Implying option -1." + end + + retval = arguments.any? ? list(*arguments) : list_installed + # retval is ternary: true/false/nil + if retval.nil? && !arguments.any? + opoo "nothing to list" # special case: avoid exit code + elsif retval.nil? + raise Hbc::CaskError, "nothing to list" + elsif !retval + raise Hbc::CaskError, "listing incomplete" + end + end + + def self.list(*cask_tokens) + count = 0 + + cask_tokens.each do |cask_token| + odebug "Listing files for Cask #{cask_token}" + begin + cask = Hbc.load(cask_token) + + if cask.installed? + if @options[:one] + puts cask.token + elsif @options[:versions] + puts format_versioned(cask) + else + installed_caskfile = cask.metadata_master_container_path.join(*cask.timestamped_versions.last, "Casks", "#{cask_token}.rb") + cask = Hbc.load(installed_caskfile) + list_artifacts(cask) + end + + count += 1 + else + opoo "#{cask} is not installed" + end + rescue Hbc::CaskUnavailableError => e + onoe e + end + end + + count == 0 ? nil : count == cask_tokens.length + end + + def self.list_artifacts(cask) + Hbc::Artifact.for_cask(cask).each do |artifact| + summary = artifact.new(cask).summary + ohai summary[:english_description], summary[:contents] unless summary.empty? + end + end + + def self.list_installed + installed_casks = Hbc.installed + + if @options[:one] + puts installed_casks.map(&:to_s) + elsif @options[:versions] + puts installed_casks.map(&method(:format_versioned)) + else + puts_columns installed_casks.map(&:to_s) + end + + installed_casks.empty? ? nil : true + end + + def self.format_versioned(cask) + cask.to_s.concat(cask.versions.map(&:to_s).join(" ").prepend(" ")) + end + + def self.help + "with no args, lists installed Casks; given installed Casks, lists staged files" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/search.rb b/Library/Homebrew/cask/lib/hbc/cli/search.rb new file mode 100644 index 0000000000..c356128a68 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/search.rb @@ -0,0 +1,56 @@ +class Hbc::CLI::Search < Hbc::CLI::Base + def self.run(*arguments) + render_results(*search(*arguments)) + end + + def self.extract_regexp(string) + if string =~ %r{^/(.*)/$} + Regexp.last_match[1] + else + false + end + end + + def self.search(*arguments) + exact_match = nil + partial_matches = [] + search_term = arguments.join(" ") + search_regexp = extract_regexp arguments.first + if search_regexp + search_term = arguments.first + partial_matches = Hbc::CLI.nice_listing(Hbc.all_tokens).grep(%r{#{search_regexp}}i) + else + # suppressing search of the font Tap is a quick hack until behavior can be made configurable + all_tokens = Hbc::CLI.nice_listing Hbc.all_tokens.reject { |t| %r{^caskroom/homebrew-fonts/}.match(t) } + simplified_tokens = all_tokens.map { |t| t.sub(%r{^.*\/}, "").gsub(%r{[^a-z0-9]+}i, "") } + simplified_search_term = search_term.sub(%r{\.rb$}i, "").gsub(%r{[^a-z0-9]+}i, "") + exact_match = simplified_tokens.grep(%r{^#{simplified_search_term}$}i) { |t| all_tokens[simplified_tokens.index(t)] }.first + partial_matches = simplified_tokens.grep(%r{#{simplified_search_term}}i) { |t| all_tokens[simplified_tokens.index(t)] } + partial_matches.delete(exact_match) + end + [exact_match, partial_matches, search_term] + end + + def self.render_results(exact_match, partial_matches, search_term) + if !exact_match && partial_matches.empty? + puts "No Cask found for \"#{search_term}\"." + return + end + if exact_match + ohai "Exact match" + puts exact_match + end + unless partial_matches.empty? + if extract_regexp search_term + ohai "Regexp matches" + else + ohai "Partial matches" + end + puts_columns partial_matches + end + end + + def self.help + "searches all known Casks" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/style.rb b/Library/Homebrew/cask/lib/hbc/cli/style.rb new file mode 100644 index 0000000000..ac7cbfb44b --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/style.rb @@ -0,0 +1,69 @@ +require "English" + +class Hbc::CLI::Style < Hbc::CLI::Base + def self.help + "checks Cask style using RuboCop" + end + + def self.run(*args) + retval = new(args).run + raise Hbc::CaskError, "style check failed" unless retval + end + + attr_reader :args + def initialize(args) + @args = args + end + + def run + install_rubocop + system "rubocop", *rubocop_args, "--", *cask_paths + $CHILD_STATUS.success? + end + + RUBOCOP_CASK_VERSION = "~> 0.8.3".freeze + + def install_rubocop + Hbc::Utils.capture_stderr do + begin + Homebrew.install_gem_setup_path! "rubocop-cask", RUBOCOP_CASK_VERSION, "rubocop" + rescue SystemExit + raise Hbc::CaskError, $stderr.string.chomp.sub("#{::Tty.red}Error#{::Tty.reset}: ", "") + end + end + end + + def cask_paths + @cask_paths ||= if cask_tokens.empty? + Hbc.all_tapped_cask_dirs + elsif cask_tokens.any? { |file| File.exist?(file) } + cask_tokens + else + cask_tokens.map { |token| Hbc.path(token) } + end + end + + def cask_tokens + @cask_tokens ||= self.class.cask_tokens_from(args) + end + + def rubocop_args + fix? ? autocorrect_args : default_args + end + + def default_args + ["--format", "simple", "--force-exclusion", "--config", rubocop_config] + end + + def autocorrect_args + default_args + ["--auto-correct"] + end + + def rubocop_config + Hbc.default_tap.cask_dir.join(".rubocop.yml") + end + + def fix? + args.any? { |arg| arg =~ %r{--(fix|(auto-?)?correct)} } + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/uninstall.rb b/Library/Homebrew/cask/lib/hbc/cli/uninstall.rb new file mode 100644 index 0000000000..cd98b6e616 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/uninstall.rb @@ -0,0 +1,40 @@ +class Hbc::CLI::Uninstall < Hbc::CLI::Base + def self.run(*args) + cask_tokens = cask_tokens_from(args) + raise Hbc::CaskUnspecifiedError if cask_tokens.empty? + force = args.include? "--force" + + cask_tokens.each do |cask_token| + odebug "Uninstalling Cask #{cask_token}" + cask = Hbc.load(cask_token) + + raise Hbc::CaskNotInstalledError, cask unless cask.installed? || force + + latest_installed_version = cask.timestamped_versions.last + + unless latest_installed_version.nil? + latest_installed_cask_file = cask.metadata_master_container_path + .join(latest_installed_version.join(File::Separator), + "Casks", "#{cask_token}.rb") + + # use the same cask file that was used for installation, if possible + cask = Hbc.load(latest_installed_cask_file) if latest_installed_cask_file.exist? + end + + Hbc::Installer.new(cask, force: force).uninstall + + next if (versions = cask.versions).empty? + + single = versions.count == 1 + + puts <<-EOF.undent + #{cask_token} #{versions.join(', ')} #{single ? 'is' : 'are'} still installed. + Remove #{single ? 'it' : 'them all'} with `brew cask uninstall --force #{cask_token}`. + EOF + end + end + + def self.help + "uninstalls the given Cask" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/update.rb b/Library/Homebrew/cask/lib/hbc/cli/update.rb new file mode 100644 index 0000000000..ceb9475442 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/update.rb @@ -0,0 +1,16 @@ +class Hbc::CLI::Update < Hbc::CLI::Base + def self.run(*_ignored) + result = Hbc::SystemCommand.run(Hbc.homebrew_executable, + args: %w[update]) + # TODO: separating stderr/stdout is undesirable here. + # Hbc::SystemCommand should have an option for plain + # unbuffered output. + print result.stdout + $stderr.print result.stderr + exit result.exit_status + end + + def self.help + "a synonym for 'brew update'" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/cli/zap.rb b/Library/Homebrew/cask/lib/hbc/cli/zap.rb new file mode 100644 index 0000000000..0813783309 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/cli/zap.rb @@ -0,0 +1,15 @@ +class Hbc::CLI::Zap < Hbc::CLI::Base + def self.run(*args) + cask_tokens = cask_tokens_from(args) + raise Hbc::CaskUnspecifiedError if cask_tokens.empty? + cask_tokens.each do |cask_token| + odebug "Zapping Cask #{cask_token}" + cask = Hbc.load(cask_token) + Hbc::Installer.new(cask).zap + end + end + + def self.help + "zaps all files associated with the given Cask" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container.rb b/Library/Homebrew/cask/lib/hbc/container.rb new file mode 100644 index 0000000000..e2b21a3ef0 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container.rb @@ -0,0 +1,68 @@ +class Hbc::Container; end + +require "hbc/container/base" +require "hbc/container/air" +require "hbc/container/bzip2" +require "hbc/container/cab" +require "hbc/container/criteria" +require "hbc/container/dmg" +require "hbc/container/generic_unar" +require "hbc/container/gzip" +require "hbc/container/lzma" +require "hbc/container/naked" +require "hbc/container/otf" +require "hbc/container/pkg" +require "hbc/container/seven_zip" +require "hbc/container/sit" +require "hbc/container/tar" +require "hbc/container/ttf" +require "hbc/container/rar" +require "hbc/container/xar" +require "hbc/container/xip" +require "hbc/container/xz" +require "hbc/container/zip" + +class Hbc::Container + def self.autodetect_containers + [ + Hbc::Container::Pkg, + Hbc::Container::Ttf, + Hbc::Container::Otf, + Hbc::Container::Air, + Hbc::Container::Cab, + Hbc::Container::Dmg, + Hbc::Container::SevenZip, + Hbc::Container::Sit, + Hbc::Container::Rar, + Hbc::Container::Zip, + Hbc::Container::Xip, # needs to be before xar as this is a cpio inside a gzip inside a xar + Hbc::Container::Xar, # need to be before tar as tar can also list xar + Hbc::Container::Tar, # or compressed tar (bzip2/gzip/lzma/xz) + Hbc::Container::Bzip2, # pure bzip2 + Hbc::Container::Gzip, # pure gzip + Hbc::Container::Lzma, # pure lzma + Hbc::Container::Xz, # pure xz + ] + # for explicit use only (never autodetected): + # Hbc::Container::Naked + # Hbc::Container::GenericUnar + end + + def self.for_path(path, command) + odebug "Determining which containers to use based on filetype" + criteria = Hbc::Container::Criteria.new(path, command) + autodetect_containers.find do |c| + odebug "Checking container class #{c}" + c.me?(criteria) + end + end + + def self.from_type(type) + odebug "Determining which containers to use based on 'container :type'" + begin + Hbc::Container.const_get(type.to_s.split("_").map(&:capitalize).join) + rescue NameError + false + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/air.rb b/Library/Homebrew/cask/lib/hbc/container/air.rb new file mode 100644 index 0000000000..e82b677e9c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/air.rb @@ -0,0 +1,33 @@ +require "hbc/container/base" + +class Hbc::Container::Air < Hbc::Container::Base + INSTALLER_PATHNAME = + Pathname("/Applications/Utilities/Adobe AIR Application Installer.app" \ + "/Contents/MacOS/Adobe AIR Application Installer") + + def self.me?(criteria) + %w[.air].include?(criteria.path.extname) + end + + def self.installer_cmd + return @installer_cmd ||= INSTALLER_PATHNAME if installer_exist? + raise Hbc::CaskError, <<-ERRMSG.undent + Adobe AIR runtime not present, try installing it via + + brew cask install adobe-air + + ERRMSG + end + + def self.installer_exist? + INSTALLER_PATHNAME.exist? + end + + def extract + install = @command.run(self.class.installer_cmd, + args: ["-silent", "-location", @cask.staged_path, Pathname.new(@path).realpath]) + + return unless install.exit_status == 9 + raise Hbc::CaskError, "Adobe AIR application #{@cask} already exists on the system, and cannot be reinstalled." + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/base.rb b/Library/Homebrew/cask/lib/hbc/container/base.rb new file mode 100644 index 0000000000..42331df31e --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/base.rb @@ -0,0 +1,37 @@ +class Hbc::Container::Base + def initialize(cask, path, command, nested: false) + @cask = cask + @path = path + @command = command + @nested = nested + end + + def extract_nested_inside(dir) + children = Pathname.new(dir).children + + nested_container = children[0] + + unless children.count == 1 && + !nested_container.directory? && + @cask.artifacts[:nested_container].empty? && + extract_nested_container(nested_container) + + children.each do |src| + dest = @cask.staged_path.join(src.basename) + FileUtils.rm_r(dest) if dest.exist? + FileUtils.mv(src, dest) + end + end + end + + def extract_nested_container(source) + container = Hbc::Container.for_path(source, @command) + + return false unless container + + ohai "Extracting nested container #{source.basename}" + container.new(@cask, source, @command, nested: true).extract + + true + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/bzip2.rb b/Library/Homebrew/cask/lib/hbc/container/bzip2.rb new file mode 100644 index 0000000000..617c68b321 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/bzip2.rb @@ -0,0 +1,18 @@ +require "tmpdir" + +require "hbc/container/base" + +class Hbc::Container::Bzip2 < Hbc::Container::Base + def self.me?(criteria) + criteria.magic_number(%r{^BZh}n) + end + + def extract + Dir.mktmpdir do |unpack_dir| + @command.run!("/usr/bin/ditto", args: ["--", @path, unpack_dir]) + @command.run!("/usr/bin/bunzip2", args: ["--quiet", "--", Pathname.new(unpack_dir).join(@path.basename)]) + + extract_nested_inside(unpack_dir) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/cab.rb b/Library/Homebrew/cask/lib/hbc/container/cab.rb new file mode 100644 index 0000000000..28000a5a3f --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/cab.rb @@ -0,0 +1,26 @@ +require "tmpdir" + +require "hbc/container/base" + +class Hbc::Container::Cab < Hbc::Container::Base + def self.me?(criteria) + cabextract = Hbc.homebrew_prefix.join("bin", "cabextract") + + criteria.magic_number(%r{^MSCF}n) && + cabextract.exist? && + criteria.command.run(cabextract, args: ["-t", "--", criteria.path.to_s]).stderr.empty? + end + + def extract + cabextract = Hbc.homebrew_prefix.join("bin", "cabextract") + + unless cabextract.exist? + raise Hbc::CaskError, "Expected to find cabextract executable. Cask '#{@cask}' must add: depends_on formula: 'cabextract'" + end + + Dir.mktmpdir do |unpack_dir| + @command.run!(cabextract, args: ["-d", unpack_dir, "--", @path]) + @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path]) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/criteria.rb b/Library/Homebrew/cask/lib/hbc/container/criteria.rb new file mode 100644 index 0000000000..2ebb9d6fa4 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/criteria.rb @@ -0,0 +1,18 @@ +class Hbc::Container::Criteria + attr_reader :path, :command + + def initialize(path, command) + @path = path + @command = command + end + + def extension(regex) + path.extname.sub(%r{^\.}, "") =~ Regexp.new(regex.source, regex.options | Regexp::IGNORECASE) + end + + def magic_number(regex) + # 262: length of the longest regex (currently: Hbc::Container::Tar) + @magic_number ||= File.open(@path, "rb") { |f| f.read(262) } + @magic_number =~ regex + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/dmg.rb b/Library/Homebrew/cask/lib/hbc/container/dmg.rb new file mode 100644 index 0000000000..7e4b9340da --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/dmg.rb @@ -0,0 +1,125 @@ +require "set" +require "tempfile" + +require "hbc/container/base" + +class Hbc::Container::Dmg < Hbc::Container::Base + def self.me?(criteria) + !criteria.command.run("/usr/bin/hdiutil", + # realpath is a failsafe against unusual filenames + args: ["imageinfo", Pathname.new(criteria.path).realpath], + print_stderr: false).stdout.empty? + end + + attr_reader :mounts + def initialize(*args) + super(*args) + @mounts = [] + end + + def extract + mount! + assert_mounts_found + extract_mounts + ensure + eject! + end + + def mount! + plist = @command.run!("/usr/bin/hdiutil", + # realpath is a failsafe against unusual filenames + args: %w[mount -plist -nobrowse -readonly -noidme -mountrandom /tmp] + [Pathname.new(@path).realpath], + input: %w[y]) + .plist + @mounts = mounts_from_plist(plist) + end + + def eject! + @mounts.each do |mount| + # realpath is a failsafe against unusual filenames + mountpath = Pathname.new(mount).realpath + next unless mountpath.exist? + + begin + tries ||= 2 + @command.run("/usr/sbin/diskutil", + args: ["eject", mountpath], + print_stderr: false) + + raise Hbc::CaskError, "Failed to eject #{mountpath}" if mountpath.exist? + rescue Hbc::CaskError => e + raise e if (tries -= 1).zero? + sleep 1 + retry + end + end + end + + private + + def extract_mounts + @mounts.each(&method(:extract_mount)) + end + + def extract_mount(mount) + Tempfile.open(["", ".bom"]) do |bomfile| + bomfile.close + + Tempfile.open(["", ".list"]) do |filelist| + filelist.write(bom_filelist_from_path(mount)) + filelist.close + + @command.run!("/usr/bin/mkbom", args: ["-s", "-i", filelist.path, "--", bomfile.path]) + @command.run!("/usr/bin/ditto", args: ["--bom", bomfile.path, "--", mount, @cask.staged_path]) + end + end + end + + def bom_filelist_from_path(mount) + Dir.chdir(mount) { + Dir.glob("**/*", File::FNM_DOTMATCH).map { |path| + next if skip_path?(Pathname(path)) + path == "." ? path : path.prepend("./") + }.compact.join("\n").concat("\n") + } + end + + def skip_path?(path) + dmg_metadata?(path) || system_dir_symlink?(path) + end + + # unnecessary DMG metadata + DMG_METADATA_FILES = %w[ + .background + .com.apple.timemachine.donotpresent + .DocumentRevisions-V100 + .DS_Store + .fseventsd + .MobileBackups + .Spotlight-V100 + .TemporaryItems + .Trashes + .VolumeIcon.icns + ].to_set.freeze + + def dmg_metadata?(path) + relative_root = path.sub(%r{/.*}, "") + DMG_METADATA_FILES.include?(relative_root.basename.to_s) + end + + def system_dir_symlink?(path) + # symlinks to system directories (commonly to /Applications) + path.symlink? && MacOS.system_dir?(path.readlink) + end + + def mounts_from_plist(plist) + return [] unless plist.respond_to?(:fetch) + plist.fetch("system-entities", []).map { |entity| + entity["mount-point"] + }.compact + end + + def assert_mounts_found + raise Hbc::CaskError, "No mounts found in '#{@path}'; perhaps it is a bad DMG?" if @mounts.empty? + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/generic_unar.rb b/Library/Homebrew/cask/lib/hbc/container/generic_unar.rb new file mode 100644 index 0000000000..1dcc0997ac --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/generic_unar.rb @@ -0,0 +1,26 @@ +require "tmpdir" + +require "hbc/container/base" + +class Hbc::Container::GenericUnar < Hbc::Container::Base + def self.me?(criteria) + lsar = Hbc.homebrew_prefix.join("bin", "lsar") + lsar.exist? && + criteria.command.run(lsar, + args: ["-l", "-t", "--", criteria.path], + print_stderr: false).stdout.chomp.end_with?("passed, 0 failed.") + end + + def extract + unar = Hbc.homebrew_prefix.join("bin", "unar") + + unless unar.exist? + raise Hbc::CaskError, "Expected to find unar executable. Cask #{@cask} must add: depends_on formula: 'unar'" + end + + Dir.mktmpdir do |unpack_dir| + @command.run!(unar, args: ["-force-overwrite", "-quiet", "-no-directory", "-output-directory", unpack_dir, "--", @path]) + @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path]) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/gzip.rb b/Library/Homebrew/cask/lib/hbc/container/gzip.rb new file mode 100644 index 0000000000..1d2cc1f371 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/gzip.rb @@ -0,0 +1,18 @@ +require "tmpdir" + +require "hbc/container/base" + +class Hbc::Container::Gzip < Hbc::Container::Base + def self.me?(criteria) + criteria.magic_number(%r{^\037\213}n) + end + + def extract + Dir.mktmpdir do |unpack_dir| + @command.run!("/usr/bin/ditto", args: ["--", @path, unpack_dir]) + @command.run!("/usr/bin/gunzip", args: ["--quiet", "--", Pathname.new(unpack_dir).join(@path.basename)]) + + extract_nested_inside(unpack_dir) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/lzma.rb b/Library/Homebrew/cask/lib/hbc/container/lzma.rb new file mode 100644 index 0000000000..e538b3779e --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/lzma.rb @@ -0,0 +1,23 @@ +require "tmpdir" + +require "hbc/container/base" + +class Hbc::Container::Lzma < Hbc::Container::Base + def self.me?(criteria) + criteria.magic_number(%r{^\]\000\000\200\000}n) + end + + def extract + unlzma = Hbc.homebrew_prefix.join("bin", "unlzma") + + unless unlzma.exist? + raise Hbc::CaskError, "Expected to find unlzma executable. Cask '#{@cask}' must add: depends_on formula: 'lzma'" + end + + Dir.mktmpdir do |unpack_dir| + @command.run!("/usr/bin/ditto", args: ["--", @path, unpack_dir]) + @command.run!(unlzma, args: ["-q", "--", Pathname(unpack_dir).join(@path.basename)]) + @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path]) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/naked.rb b/Library/Homebrew/cask/lib/hbc/container/naked.rb new file mode 100644 index 0000000000..596f50789c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/naked.rb @@ -0,0 +1,19 @@ +require "hbc/container/base" + +class Hbc::Container::Naked < Hbc::Container::Base + # Either inherit from this class and override with self.me?(criteria), + # or use this class directly as "container type: :naked", + # in which case self.me? is not called. + def self.me?(*) + false + end + + def extract + @command.run!("/usr/bin/ditto", args: ["--", @path, @cask.staged_path.join(target_file)]) + end + + def target_file + return @path.basename if @nested + URI.decode(File.basename(@cask.url.path)) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/otf.rb b/Library/Homebrew/cask/lib/hbc/container/otf.rb new file mode 100644 index 0000000000..f9a25e1ed0 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/otf.rb @@ -0,0 +1,7 @@ +require "hbc/container/naked" + +class Hbc::Container::Otf < Hbc::Container::Naked + def self.me?(criteria) + criteria.magic_number(%r{^OTTO}n) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/pkg.rb b/Library/Homebrew/cask/lib/hbc/container/pkg.rb new file mode 100644 index 0000000000..5d2282d0fb --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/pkg.rb @@ -0,0 +1,9 @@ +require "hbc/container/naked" + +class Hbc::Container::Pkg < Hbc::Container::Naked + def self.me?(criteria) + criteria.extension(%r{m?pkg$}) && + (criteria.path.directory? || + criteria.magic_number(%r{^xar!}n)) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/rar.rb b/Library/Homebrew/cask/lib/hbc/container/rar.rb new file mode 100644 index 0000000000..9c144006f6 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/rar.rb @@ -0,0 +1,8 @@ +require "hbc/container/generic_unar" + +class Hbc::Container::Rar < Hbc::Container::GenericUnar + def self.me?(criteria) + criteria.magic_number(%r{^Rar!}n) && + super + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/seven_zip.rb b/Library/Homebrew/cask/lib/hbc/container/seven_zip.rb new file mode 100644 index 0000000000..f0d183064e --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/seven_zip.rb @@ -0,0 +1,9 @@ +require "hbc/container/generic_unar" + +class Hbc::Container::SevenZip < Hbc::Container::GenericUnar + def self.me?(criteria) + # TODO: cover self-extracting archives + criteria.magic_number(%r{^7z}n) && + super + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/sit.rb b/Library/Homebrew/cask/lib/hbc/container/sit.rb new file mode 100644 index 0000000000..155b93f3f4 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/sit.rb @@ -0,0 +1,8 @@ +require "hbc/container/generic_unar" + +class Hbc::Container::Sit < Hbc::Container::GenericUnar + def self.me?(criteria) + criteria.magic_number(%r{^StuffIt}n) && + super + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/tar.rb b/Library/Homebrew/cask/lib/hbc/container/tar.rb new file mode 100644 index 0000000000..8bc7c5f643 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/tar.rb @@ -0,0 +1,18 @@ +require "tmpdir" + +require "hbc/container/base" + +class Hbc::Container::Tar < Hbc::Container::Base + def self.me?(criteria) + criteria.magic_number(%r{^.{257}ustar}n) || + # or compressed tar (bzip2/gzip/lzma/xz) + IO.popen(["/usr/bin/tar", "-t", "-f", criteria.path.to_s], err: "/dev/null") { |io| !io.read(1).nil? } + end + + def extract + Dir.mktmpdir do |unpack_dir| + @command.run!("/usr/bin/tar", args: ["-x", "-f", @path, "-C", unpack_dir]) + @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path]) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/ttf.rb b/Library/Homebrew/cask/lib/hbc/container/ttf.rb new file mode 100644 index 0000000000..8d787f360c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/ttf.rb @@ -0,0 +1,10 @@ +require "hbc/container/naked" + +class Hbc::Container::Ttf < Hbc::Container::Naked + def self.me?(criteria) + # TrueType Font + criteria.magic_number(%r{^\000\001\000\000\000}n) || + # Truetype Font Collection + criteria.magic_number(%r{^ttcf}n) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/xar.rb b/Library/Homebrew/cask/lib/hbc/container/xar.rb new file mode 100644 index 0000000000..5afc78bc55 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/xar.rb @@ -0,0 +1,16 @@ +require "tmpdir" + +require "hbc/container/base" + +class Hbc::Container::Xar < Hbc::Container::Base + def self.me?(criteria) + criteria.magic_number(%r{^xar!}n) + end + + def extract + Dir.mktmpdir do |unpack_dir| + @command.run!("/usr/bin/xar", args: ["-x", "-f", @path, "-C", unpack_dir]) + @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path]) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/xip.rb b/Library/Homebrew/cask/lib/hbc/container/xip.rb new file mode 100644 index 0000000000..579f28fe0d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/xip.rb @@ -0,0 +1,25 @@ +require "tmpdir" + +class Hbc::Container::Xip < Hbc::Container::Base + def self.me?(criteria) + criteria.magic_number(%r{^xar!}n) && + IO.popen(["/usr/bin/xar", "-t", "-f", criteria.path.to_s], err: "/dev/null") { |io| io.read =~ %r{\AContent\nMetadata\n\Z} } + end + + def extract + Dir.mktmpdir do |unpack_dir| + begin + ohai "Verifying signature for #{@path.basename}" + @command.run!("/usr/sbin/pkgutil", args: ["--check-signature", @path]) + rescue + raise "Signature check failed." + end + + @command.run!("/usr/bin/xar", args: ["-x", "-f", @path, "Content", "-C", unpack_dir]) + + Dir.chdir(@cask.staged_path) do + @command.run!("/usr/bin/cpio", args: ["--quiet", "-i", "-I", Pathname(unpack_dir).join("Content")]) + end + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/xz.rb b/Library/Homebrew/cask/lib/hbc/container/xz.rb new file mode 100644 index 0000000000..228532943d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/xz.rb @@ -0,0 +1,23 @@ +require "tmpdir" + +require "hbc/container/base" + +class Hbc::Container::Xz < Hbc::Container::Base + def self.me?(criteria) + criteria.magic_number(%r{^\xFD7zXZ\x00}n) + end + + def extract + unxz = Hbc.homebrew_prefix.join("bin", "unxz") + + unless unxz.exist? + raise Hbc::CaskError, "Expected to find unxz executable. Cask '#{@cask}' must add: depends_on formula: 'xz'" + end + + Dir.mktmpdir do |unpack_dir| + @command.run!("/usr/bin/ditto", args: ["--", @path, unpack_dir]) + @command.run!(unxz, args: ["-q", "--", Pathname(unpack_dir).join(@path.basename)]) + @command.run!("/usr/bin/ditto", args: ["--", unpack_dir, @cask.staged_path]) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/container/zip.rb b/Library/Homebrew/cask/lib/hbc/container/zip.rb new file mode 100644 index 0000000000..c6702fbb5e --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/container/zip.rb @@ -0,0 +1,15 @@ +require "hbc/container/base" + +class Hbc::Container::Zip < Hbc::Container::Base + def self.me?(criteria) + criteria.magic_number(%r{^PK(\003\004|\005\006)}n) + end + + def extract + Dir.mktmpdir do |unpack_dir| + @command.run!("/usr/bin/ditto", args: ["-x", "-k", "--", @path, unpack_dir]) + + extract_nested_inside(unpack_dir) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/download.rb b/Library/Homebrew/cask/lib/hbc/download.rb new file mode 100644 index 0000000000..18dd7fe44c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/download.rb @@ -0,0 +1,43 @@ +require "fileutils" +require "hbc/verify" + +class Hbc::Download + attr_reader :cask + + def initialize(cask, force: false) + @cask = cask + @force = force + end + + def perform + clear_cache + fetch + downloaded_path + end + + private + + attr_reader :force + attr_accessor :downloaded_path + + def downloader + @downloader ||= case cask.url.using + when :svn + Hbc::SubversionDownloadStrategy.new(cask) + when :post + Hbc::CurlPostDownloadStrategy.new(cask) + else + Hbc::CurlDownloadStrategy.new(cask) + end + end + + def clear_cache + downloader.clear_cache if force || cask.version.latest? + end + + def fetch + self.downloaded_path = downloader.fetch + rescue StandardError => e + raise Hbc::CaskError, "Download failed on Cask '#{cask}' with message: #{e}" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/download_strategy.rb b/Library/Homebrew/cask/lib/hbc/download_strategy.rb new file mode 100644 index 0000000000..88ffb50505 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/download_strategy.rb @@ -0,0 +1,332 @@ +require "cgi" + +# We abuse Homebrew's download strategies considerably here. +# * Our downloader instances only invoke the fetch and +# clear_cache methods, ignoring stage +# * Our overridden fetch methods are expected to return +# a value: the successfully downloaded file. + +class Hbc::AbstractDownloadStrategy + attr_reader :cask, :name, :url, :uri_object, :version + + def initialize(cask, command = Hbc::SystemCommand) + @cask = cask + @command = command + # TODO: this excess of attributes is a function of integrating + # with Homebrew's classes. Later we should be able to remove + # these in favor of @cask + @name = cask.token + @url = cask.url.to_s + @uri_object = cask.url + @version = cask.version + end + + # All download strategies are expected to implement these methods + def fetch; end + + def cached_location; end + + def clear_cache; end +end + +class Hbc::HbVCSDownloadStrategy < Hbc::AbstractDownloadStrategy + REF_TYPES = [:branch, :revision, :revisions, :tag].freeze + + def initialize(cask, command = Hbc::SystemCommand) + super + @ref_type, @ref = extract_ref + @clone = Hbc.cache.join(cache_filename) + end + + def extract_ref + key = REF_TYPES.find { |type| + uri_object.respond_to?(type) && uri_object.send(type) + } + [key, key ? uri_object.send(key) : nil] + end + + def cache_filename + "#{name}--#{cache_tag}" + end + + def cache_tag + "__UNKNOWN__" + end + + def cached_location + @clone + end + + def clear_cache + cached_location.rmtree if cached_location.exist? + end +end + +class Hbc::CurlDownloadStrategy < Hbc::AbstractDownloadStrategy + # TODO: should be part of url object + def mirrors + @mirrors ||= [] + end + + def tarball_path + @tarball_path ||= Hbc.cache.join("#{name}--#{version}#{ext}") + end + + def temporary_path + @temporary_path ||= tarball_path.sub(%r{$}, ".incomplete") + end + + def cached_location + tarball_path + end + + def clear_cache + [cached_location, temporary_path].each do |f| + next unless f.exist? + raise CurlDownloadStrategyError, "#{f} is in use by another process" if Hbc::Utils.file_locked?(f) + f.unlink + end + end + + def downloaded_size + temporary_path.size? || 0 + end + + def _fetch + odebug "Calling curl with args #{cask_curl_args.utf8_inspect}" + curl(*cask_curl_args) + end + + def fetch + ohai "Downloading #{@url}" + if tarball_path.exist? + puts "Already downloaded: #{tarball_path}" + else + had_incomplete_download = temporary_path.exist? + begin + File.open(temporary_path, "w+") do |f| + f.flock(File::LOCK_EX) + _fetch + f.flock(File::LOCK_UN) + end + rescue ErrorDuringExecution + # 33 == range not supported + # try wiping the incomplete download and retrying once + if $CHILD_STATUS.exitstatus == 33 && had_incomplete_download + ohai "Trying a full download" + temporary_path.unlink + had_incomplete_download = false + retry + end + + msg = @url + msg.concat("\nThe incomplete download is cached at #{temporary_path}") if temporary_path.exist? + raise CurlDownloadStrategyError, msg + end + ignore_interrupts { temporary_path.rename(tarball_path) } + end + tarball_path + rescue CurlDownloadStrategyError + raise if mirrors.empty? + puts "Trying a mirror..." + @url = mirrors.shift + retry + end + + private + + def cask_curl_args + default_curl_args.tap do |args| + args.concat(user_agent_args) + args.concat(cookies_args) + args.concat(referer_args) + end + end + + def default_curl_args + [url, "-C", downloaded_size, "-o", temporary_path] + end + + def user_agent_args + if uri_object.user_agent + ["-A", uri_object.user_agent] + else + [] + end + end + + def cookies_args + if uri_object.cookies + [ + "-b", + # sort_by is for predictability between Ruby versions + uri_object + .cookies + .sort_by(&:to_s) + .map { |key, value| "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}" } + .join(";"), + ] + else + [] + end + end + + def referer_args + if uri_object.referer + ["-e", uri_object.referer] + else + [] + end + end + + def ext + Pathname.new(@url).extname + end +end + +class Hbc::CurlPostDownloadStrategy < Hbc::CurlDownloadStrategy + def cask_curl_args + super + default_curl_args.concat(post_args) + end + + def post_args + if uri_object.data + # sort_by is for predictability between Ruby versions + uri_object + .data + .sort_by(&:to_s) + .map { |key, value| ["-d", "#{CGI.escape(key.to_s)}=#{CGI.escape(value.to_s)}"] } + .flatten + else + ["-X", "POST"] + end + end +end + +class Hbc::SubversionDownloadStrategy < Hbc::HbVCSDownloadStrategy + def cache_tag + # TODO: pass versions as symbols, support :head here + version == "head" ? "svn-HEAD" : "svn" + end + + def repo_valid? + @clone.join(".svn").directory? + end + + def repo_url + `svn info '#{@clone}' 2>/dev/null`.strip[%r{^URL: (.+)$}, 1] + end + + # super does not provide checks for already-existing downloads + def fetch + if tarball_path.exist? + puts "Already downloaded: #{tarball_path}" + else + @url = @url.sub(%r{^svn\+}, "") if @url =~ %r{^svn\+http://} + ohai "Checking out #{@url}" + + clear_cache unless @url.chomp("/") == repo_url || quiet_system("svn", "switch", @url, @clone) + + if @clone.exist? && !repo_valid? + puts "Removing invalid SVN repo from cache" + clear_cache + end + + case @ref_type + when :revision + fetch_repo @clone, @url, @ref + when :revisions + # nil is OK for main_revision, as fetch_repo will then get latest + main_revision = @ref[:trunk] + fetch_repo @clone, @url, main_revision, true + + fetch_externals do |external_name, external_url| + fetch_repo @clone + external_name, external_url, @ref[external_name], true + end + else + fetch_repo @clone, @url + end + compress + end + tarball_path + end + + # This primary reason for redefining this method is the trust_cert + # option, controllable from the Cask definition. We also force + # consistent timestamps. The rest of this method is similar to + # Homebrew's, but translated to local idiom. + def fetch_repo(target, url, revision = uri_object.revision, ignore_externals = false) + # Use "svn up" when the repository already exists locally. + # This saves on bandwidth and will have a similar effect to verifying the + # cache as it will make any changes to get the right revision. + svncommand = target.directory? ? "up" : "checkout" + args = [svncommand] + + # SVN shipped with XCode 3.1.4 can't force a checkout. + args << "--force" unless MacOS.version == :leopard + + # make timestamps consistent for checksumming + args.concat(%w[--config-option config:miscellany:use-commit-times=yes]) + + if uri_object.trust_cert + args << "--trust-server-cert" + args << "--non-interactive" + end + + args << url unless target.directory? + args << target + args << "-r" << revision if revision + args << "--ignore-externals" if ignore_externals + @command.run!("/usr/bin/svn", + args: args, + print_stderr: false) + end + + def tarball_path + @tarball_path ||= cached_location.dirname.join(cached_location.basename.to_s + "-#{@cask.version}.tar") + end + + def shell_quote(str) + # Oh god escaping shell args. + # See http://notetoself.vrensk.com/2008/08/escaping-single-quotes-in-ruby-harder-than-expected/ + str.gsub(%r{\\|'}) { |c| "\\#{c}" } + end + + def fetch_externals + `svn propget svn:externals '#{shell_quote(@url)}'`.chomp.each_line do |line| + name, url = line.split(%r{\s+}) + yield name, url + end + end + + private + + # TODO/UPDATE: the tar approach explained below is fragile + # against challenges such as case-sensitive filesystems, + # and must be re-implemented. + # + # Seems nutty: we "download" the contents into a tape archive. + # Why? + # * A single file is tractable to the rest of the Cask toolchain, + # * An alternative would be to create a Directory container type. + # However, some type of file-serialization trick would still be + # needed in order to enable calculating a single checksum over + # a directory. So, in that alternative implementation, the + # special cases would propagate outside this class, including + # the use of tar or equivalent. + # * SubversionDownloadStrategy.cached_location is not versioned + # * tarball_path provides a needed return value for our overridden + # fetch method. + # * We can also take this private opportunity to strip files from + # the download which are protocol-specific. + + def compress + Dir.chdir(cached_location) do + @command.run!("/usr/bin/tar", + args: ['-s/^\.//', "--exclude", ".svn", "-cf", Pathname.new(tarball_path), "--", "."], + print_stderr: false) + end + clear_cache + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl.rb b/Library/Homebrew/cask/lib/hbc/dsl.rb new file mode 100644 index 0000000000..f390125421 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl.rb @@ -0,0 +1,283 @@ +require "set" + +class Hbc::DSL; end + +require "hbc/dsl/appcast" +require "hbc/dsl/base" +require "hbc/dsl/caveats" +require "hbc/dsl/conflicts_with" +require "hbc/dsl/container" +require "hbc/dsl/depends_on" +require "hbc/dsl/gpg" +require "hbc/dsl/installer" +require "hbc/dsl/license" +require "hbc/dsl/postflight" +require "hbc/dsl/preflight" +require "hbc/dsl/stanza_proxy" +require "hbc/dsl/uninstall_postflight" +require "hbc/dsl/uninstall_preflight" +require "hbc/dsl/version" + +class Hbc::DSL + ORDINARY_ARTIFACT_TYPES = [ + :app, + :artifact, + :audio_unit_plugin, + :binary, + :colorpicker, + :font, + :input_method, + :internet_plugin, + :pkg, + :prefpane, + :qlplugin, + :screen_saver, + :service, + :stage_only, + :suite, + :vst_plugin, + :vst3_plugin, + ].freeze + + ACTIVATABLE_ARTIFACT_TYPES = ([:installer, *ORDINARY_ARTIFACT_TYPES] - [:stage_only]).freeze + + SPECIAL_ARTIFACT_TYPES = [ + :uninstall, + :zap, + ].freeze + + ARTIFACT_BLOCK_TYPES = [ + :preflight, + :postflight, + :uninstall_preflight, + :uninstall_postflight, + ].freeze + + DSL_METHODS = Set.new [ + :accessibility_access, + :appcast, + :artifacts, + :auto_updates, + :caskroom_path, + :caveats, + :conflicts_with, + :container, + :depends_on, + :gpg, + :homepage, + :license, + :name, + :sha256, + :staged_path, + :url, + :version, + :appdir, + *ORDINARY_ARTIFACT_TYPES, + *ACTIVATABLE_ARTIFACT_TYPES, + *SPECIAL_ARTIFACT_TYPES, + *ARTIFACT_BLOCK_TYPES, + ].freeze + + attr_reader :token + def initialize(token) + @token = token + end + + def name(*args) + @name ||= [] + return @name if args.empty? + @name.concat(args.flatten) + end + + def assert_only_one_stanza_allowed(stanza, arg_given) + return unless instance_variable_defined?("@#{stanza}") && arg_given + raise Hbc::CaskInvalidError.new(token, "'#{stanza}' stanza may only appear once") + end + + def homepage(homepage = nil) + assert_only_one_stanza_allowed :homepage, !homepage.nil? + @homepage ||= homepage + end + + def url(*args, &block) + url_given = !args.empty? || block_given? + return @url unless url_given + assert_only_one_stanza_allowed :url, url_given + @url ||= begin + Hbc::URL.from(*args, &block) + rescue StandardError => e + raise Hbc::CaskInvalidError.new(token, "'url' stanza failed with: #{e}") + end + end + + def appcast(*args) + return @appcast if args.empty? + assert_only_one_stanza_allowed :appcast, !args.empty? + @appcast ||= begin + Hbc::DSL::Appcast.new(*args) unless args.empty? + rescue StandardError => e + raise Hbc::CaskInvalidError.new(token, e) + end + end + + def gpg(*args) + return @gpg if args.empty? + assert_only_one_stanza_allowed :gpg, !args.empty? + @gpg ||= begin + Hbc::DSL::Gpg.new(*args) unless args.empty? + rescue StandardError => e + raise Hbc::CaskInvalidError.new(token, e) + end + end + + def container(*args) + return @container if args.empty? + # TODO: remove this constraint, and instead merge multiple container stanzas + assert_only_one_stanza_allowed :container, !args.empty? + @container ||= begin + Hbc::DSL::Container.new(*args) unless args.empty? + rescue StandardError => e + raise Hbc::CaskInvalidError.new(token, e) + end + # TODO: remove this backward-compatibility section after removing nested_container + if @container && @container.nested + artifacts[:nested_container] << @container.nested + end + @container + end + + SYMBOLIC_VERSIONS = Set.new [ + :latest, + ] + + def version(arg = nil) + return @version if arg.nil? + assert_only_one_stanza_allowed :version, !arg.nil? + raise Hbc::CaskInvalidError.new(token, "invalid 'version' value: '#{arg.inspect}'") if !arg.is_a?(String) && !SYMBOLIC_VERSIONS.include?(arg) + @version ||= Hbc::DSL::Version.new(arg) + end + + SYMBOLIC_SHA256S = Set.new [ + :no_check, + ] + + def sha256(arg = nil) + return @sha256 if arg.nil? + assert_only_one_stanza_allowed :sha256, !arg.nil? + raise Hbc::CaskInvalidError.new(token, "invalid 'sha256' value: '#{arg.inspect}'") if !arg.is_a?(String) && !SYMBOLIC_SHA256S.include?(arg) + @sha256 ||= arg + end + + def license(arg = nil) + return @license if arg.nil? + assert_only_one_stanza_allowed :license, !arg.nil? + @license ||= begin + Hbc::DSL::License.new(arg) unless arg.nil? + rescue StandardError => e + raise Hbc::CaskInvalidError.new(token, e) + end + end + + # depends_on uses a load method so that multiple stanzas can be merged + def depends_on(*args) + return @depends_on if args.empty? + @depends_on ||= Hbc::DSL::DependsOn.new + begin + @depends_on.load(*args) unless args.empty? + rescue RuntimeError => e + raise Hbc::CaskInvalidError.new(token, e) + end + @depends_on + end + + def conflicts_with(*args) + return @conflicts_with if args.empty? + # TODO: remove this constraint, and instead merge multiple conflicts_with stanzas + assert_only_one_stanza_allowed :conflicts_with, !args.empty? + @conflicts_with ||= begin + Hbc::DSL::ConflictsWith.new(*args) unless args.empty? + rescue StandardError => e + raise Hbc::CaskInvalidError.new(token, e) + end + end + + def artifacts + @artifacts ||= Hash.new { |hash, key| hash[key] = Set.new } + end + + def caskroom_path + @caskroom_path ||= Hbc.caskroom.join(token) + end + + def staged_path + return @staged_path if @staged_path + cask_version = version || :unknown + @staged_path = caskroom_path.join(cask_version.to_s) + end + + def caveats(*string, &block) + @caveats ||= [] + if block_given? + @caveats << Hbc::Caveats.new(block) + elsif string.any? + @caveats << string.map { |s| s.to_s.sub(%r{[\r\n \t]*\Z}, "\n\n") } + end + @caveats + end + + def accessibility_access(accessibility_access = nil) + assert_only_one_stanza_allowed :accessibility_access, !accessibility_access.nil? + @accessibility_access ||= accessibility_access + end + + def auto_updates(auto_updates = nil) + assert_only_one_stanza_allowed :auto_updates, !auto_updates.nil? + @auto_updates ||= auto_updates + end + + ORDINARY_ARTIFACT_TYPES.each do |type| + define_method(type) do |*args| + if type == :stage_only && args != [true] + raise Hbc::CaskInvalidError.new(token, "'stage_only' takes a single argument: true") + end + artifacts[type] << args + if artifacts.key?(:stage_only) && artifacts.keys.count > 1 && + !(artifacts.keys & ACTIVATABLE_ARTIFACT_TYPES).empty? + raise Hbc::CaskInvalidError.new(token, "'stage_only' must be the only activatable artifact") + end + end + end + + def installer(*args) + return artifacts[:installer] if args.empty? + artifacts[:installer] << Hbc::DSL::Installer.new(*args) + raise "'stage_only' must be the only activatable artifact" if artifacts.key?(:stage_only) + rescue StandardError => e + raise Hbc::CaskInvalidError.new(token, e) + end + + SPECIAL_ARTIFACT_TYPES.each do |type| + define_method(type) do |*args| + artifacts[type].merge(args) + end + end + + ARTIFACT_BLOCK_TYPES.each do |type| + define_method(type) do |&block| + artifacts[type] << block + end + end + + def method_missing(method, *) + Hbc::Utils.method_missing_message(method, token) + nil + end + + def appdir + self.class.appdir + end + + def self.appdir + Hbc.appdir.sub(%r{\/$}, "") + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/appcast.rb b/Library/Homebrew/cask/lib/hbc/dsl/appcast.rb new file mode 100644 index 0000000000..b02616cfe3 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/appcast.rb @@ -0,0 +1,17 @@ +class Hbc::DSL::Appcast + attr_reader :parameters, :checkpoint + + def initialize(uri, parameters = {}) + @parameters = parameters + @uri = Hbc::UnderscoreSupportingURI.parse(uri) + @checkpoint = @parameters[:checkpoint] + end + + def to_yaml + [@uri, @parameters].to_yaml + end + + def to_s + @uri.to_s + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/base.rb b/Library/Homebrew/cask/lib/hbc/dsl/base.rb new file mode 100644 index 0000000000..4bf62014e6 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/base.rb @@ -0,0 +1,21 @@ +class Hbc::DSL::Base + extend Forwardable + + def initialize(cask, command = Hbc::SystemCommand) + @cask = cask + @command = command + end + + def_delegators :@cask, :token, :version, :caskroom_path, :staged_path, :appdir + + def system_command(executable, options = {}) + @command.run!(executable, options) + end + + def method_missing(method, *) + underscored_class = self.class.name.gsub(%r{([[:lower:]])([[:upper:]][[:lower:]])}, '\1_\2').downcase + section = underscored_class.downcase.split("::").last + Hbc::Utils.method_missing_message(method, @cask.to_s, section) + nil + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/caveats.rb b/Library/Homebrew/cask/lib/hbc/dsl/caveats.rb new file mode 100644 index 0000000000..d872f49cb9 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/caveats.rb @@ -0,0 +1,112 @@ +# Caveats DSL. Each method should handle output, following the +# convention of at least one trailing blank line so that the user +# can distinguish separate caveats. +# +# ( The return value of the last method in the block is also sent +# to the output by the caller, but that feature is only for the +# convenience of Cask authors. ) +class Hbc::DSL::Caveats < Hbc::DSL::Base + def path_environment_variable(path) + puts <<-EOS.undent + To use #{@cask}, you may need to add the #{path} directory + to your PATH environment variable, eg (for bash shell): + + export PATH=#{path}:"$PATH" + + EOS + end + + def zsh_path_helper(path) + puts <<-EOS.undent + To use #{@cask}, zsh users may need to add the following line to their + ~/.zprofile. (Among other effects, #{path} will be added to the + PATH environment variable): + + eval `/usr/libexec/path_helper -s` + + EOS + end + + def files_in_usr_local + localpath = "/usr/local" + return unless Hbc.homebrew_prefix.to_s.downcase.start_with?(localpath) + puts <<-EOS.undent + Cask #{@cask} installs files under "#{localpath}". The presence of such + files can cause warnings when running "brew doctor", which is considered + to be a bug in Homebrew-Cask. + + EOS + end + + def depends_on_java(java_version = "any") + if java_version == "any" + puts <<-EOS.undent + #{@cask} requires Java. You can install the latest version with + + brew cask install java + + EOS + elsif java_version.include?("8") || java_version.include?("+") + puts <<-EOS.undent + #{@cask} requires Java #{java_version}. You can install the latest version with + + brew cask install java + + EOS + else + puts <<-EOS.undent + #{@cask} requires Java #{java_version}. You can install it with + + brew cask install caskroom/versions/java#{java_version} + + EOS + end + end + + def logout + puts <<-EOS.undent + You must log out and log back in for the installation of #{@cask} + to take effect. + + EOS + end + + def reboot + puts <<-EOS.undent + You must reboot for the installation of #{@cask} to take effect. + + EOS + end + + def discontinued + puts <<-EOS.undent + #{@cask} has been officially discontinued upstream. + It may stop working correctly (or at all) in recent versions of macOS. + + EOS + end + + def free_license(web_page) + puts <<-EOS.undent + The vendor offers a free license for #{@cask} at + #{web_page} + + EOS + end + + def malware(radar_number) + puts <<-EOS.undent + #{@cask} has been reported to bundle malware. Like with any app, use at your own risk. + + A report has been made to Apple about this app. Their certificate will hopefully be revoked. + See the public report at + https://openradar.appspot.com/#{radar_number} + + If this report is accurate, please duplicate it at + https://bugreport.apple.com/ + If this report is a mistake, please let us know by opening an issue at + https://github.com/caskroom/homebrew-cask/issues/new + + EOS + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/conflicts_with.rb b/Library/Homebrew/cask/lib/hbc/dsl/conflicts_with.rb new file mode 100644 index 0000000000..b2de2cd45e --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/conflicts_with.rb @@ -0,0 +1,30 @@ +class Hbc::DSL::ConflictsWith + VALID_KEYS = Set.new [ + :formula, + :cask, + :macos, + :arch, + :x11, + :java, + ] + + attr_accessor(*VALID_KEYS) + attr_accessor :pairs + + def initialize(pairs = {}) + @pairs = pairs + pairs.each do |key, value| + raise "invalid conflicts_with key: '#{key.inspect}'" unless VALID_KEYS.include?(key) + writer_method = "#{key}=".to_sym + send(writer_method, value) + end + end + + def to_yaml + @pairs.to_yaml + end + + def to_s + @pairs.inspect + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/container.rb b/Library/Homebrew/cask/lib/hbc/dsl/container.rb new file mode 100644 index 0000000000..39f1566686 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/container.rb @@ -0,0 +1,26 @@ +class Hbc::DSL::Container + VALID_KEYS = Set.new [ + :type, + :nested, + ] + + attr_accessor(*VALID_KEYS) + attr_accessor :pairs + + def initialize(pairs = {}) + @pairs = pairs + pairs.each do |key, value| + raise "invalid container key: '#{key.inspect}'" unless VALID_KEYS.include?(key) + writer_method = "#{key}=".to_sym + send(writer_method, value) + end + end + + def to_yaml + @pairs.to_yaml + end + + def to_s + @pairs.inspect + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/depends_on.rb b/Library/Homebrew/cask/lib/hbc/dsl/depends_on.rb new file mode 100644 index 0000000000..a7dba36439 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/depends_on.rb @@ -0,0 +1,124 @@ +require "rubygems" + +class Hbc::DSL::DependsOn + VALID_KEYS = Set.new [ + :formula, + :cask, + :macos, + :arch, + :x11, + :java, + ].freeze + + VALID_ARCHES = { + intel: { type: :intel, bits: [32, 64] }, + ppc: { type: :ppc, bits: [32, 64] }, + # specific + i386: { type: :intel, bits: 32 }, + x86_64: { type: :intel, bits: 64 }, + ppc_7400: { type: :ppc, bits: 32 }, + ppc_64: { type: :ppc, bits: 64 }, + }.freeze + + # Intentionally undocumented: catch variant spellings. + ARCH_SYNONYMS = { + x86_32: :i386, + x8632: :i386, + x8664: :x86_64, + intel_32: :i386, + intel32: :i386, + intel_64: :x86_64, + intel64: :x86_64, + amd_64: :x86_64, + amd64: :x86_64, + ppc7400: :ppc_7400, + ppc_32: :ppc_7400, + ppc32: :ppc_7400, + ppc64: :ppc_64, + }.freeze + + attr_accessor :java + attr_accessor :pairs + attr_reader :arch, :cask, :formula, :macos, :x11 + + def initialize + @pairs ||= {} + end + + def load(pairs = {}) + pairs.each do |key, value| + raise "invalid depends_on key: '#{key.inspect}'" unless VALID_KEYS.include?(key) + writer_method = "#{key}=".to_sym + @pairs[key] = send(writer_method, value) + end + end + + def self.coerce_os_release(arg) + @macos_symbols ||= MacOS::Version::SYMBOLS + @inverted_macos_symbols ||= @macos_symbols.invert + + begin + if arg.is_a?(Symbol) + Gem::Version.new(@macos_symbols.fetch(arg)) + elsif arg =~ %r{^\s*:?([a-z]\S+)\s*$}i + Gem::Version.new(@macos_symbols.fetch(Regexp.last_match[1].downcase.to_sym)) + elsif @inverted_macos_symbols.key?(arg) + Gem::Version.new(arg) + else + raise + end + rescue StandardError + raise "invalid 'depends_on macos' value: #{arg.inspect}" + end + end + + def formula=(*arg) + @formula ||= [] + @formula.concat(Array(*arg)) + end + + def cask=(*arg) + @cask ||= [] + @cask.concat(Array(*arg)) + end + + def macos=(*arg) + @macos ||= [] + macos = if arg.count == 1 && arg.first =~ %r{^\s*(<|>|[=<>]=)\s*(\S+)\s*$} + raise "'depends_on macos' comparison expressions cannot be combined" unless @macos.empty? + operator = Regexp.last_match[1].to_sym + release = self.class.coerce_os_release(Regexp.last_match[2]) + [[operator, release]] + else + raise "'depends_on macos' comparison expressions cannot be combined" if @macos.first.is_a?(Symbol) + Array(*arg).map { |elt| + self.class.coerce_os_release(elt) + }.sort + end + @macos.concat(macos) + end + + def arch=(*arg) + @arch ||= [] + arches = Array(*arg).map { |elt| + elt = elt.to_s.downcase.sub(%r{^:}, "").tr("-", "_").to_sym + ARCH_SYNONYMS.key?(elt) ? ARCH_SYNONYMS[elt] : elt + } + invalid_arches = arches - VALID_ARCHES.keys + raise "invalid 'depends_on arch' values: #{invalid_arches.inspect}" unless invalid_arches.empty? + @arch.concat(arches.map { |arch| VALID_ARCHES[arch] }) + end + + def x11=(arg) + raise "invalid 'depends_on x11' value: #{arg.inspect}" unless [true, false].include?(arg) + @x11 = arg + end + + def to_yaml + @pairs.to_yaml + end + + def to_s + @pairs.inspect + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/gpg.rb b/Library/Homebrew/cask/lib/hbc/dsl/gpg.rb new file mode 100644 index 0000000000..9496a8c05a --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/gpg.rb @@ -0,0 +1,43 @@ +class Hbc::DSL::Gpg + KEY_PARAMETERS = Set.new [ + :key_id, + :key_url, + ] + + VALID_PARAMETERS = Set.new [] + VALID_PARAMETERS.merge KEY_PARAMETERS + + attr_accessor(*VALID_PARAMETERS) + attr_accessor :signature + + def initialize(signature, parameters = {}) + @parameters = parameters + @signature = Hbc::UnderscoreSupportingURI.parse(signature) + parameters.each do |hkey, hvalue| + raise "invalid 'gpg' parameter: '#{hkey.inspect}'" unless VALID_PARAMETERS.include?(hkey) + writer_method = "#{hkey}=".to_sym + hvalue = Hbc::UnderscoreSupportingURI.parse(hvalue) if hkey == :key_url + valid_id?(hvalue) if hkey == :key_id + send(writer_method, hvalue) + end + return if KEY_PARAMETERS.intersection(parameters.keys).length == 1 + raise "'gpg' stanza must include exactly one of: '#{KEY_PARAMETERS.to_a}'" + end + + def valid_id?(id) + legal_lengths = Set.new [8, 16, 40] + is_valid = id.is_a?(String) && legal_lengths.include?(id.length) && id[%r{^[0-9a-f]+$}i] + raise "invalid ':key_id' value: '#{id.inspect}'" unless is_valid + + is_valid + end + + def to_yaml + # bug, :key_url value is not represented as an instance of Hbc::UnderscoreSupportingURI + [@signature, @parameters].to_yaml + end + + def to_s + @signature.to_s + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/installer.rb b/Library/Homebrew/cask/lib/hbc/dsl/installer.rb new file mode 100644 index 0000000000..74b4b3a91c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/installer.rb @@ -0,0 +1,28 @@ +class Hbc::DSL::Installer + VALID_KEYS = Set.new [ + :manual, + :script, + ] + + attr_accessor(*VALID_KEYS) + + def initialize(*parameters) + raise Hbc::CaskInvalidError.new(token, "'installer' stanza requires an argument") if parameters.empty? + parameters = {}.merge(*parameters) + if parameters.key?(:script) && !parameters[:script].respond_to?(:key?) + if parameters.key?(:executable) + raise Hbc::CaskInvalidError.new(token, "'installer' stanza gave arguments for both :script and :executable") + end + parameters[:executable] = parameters[:script] + parameters.delete(:script) + parameters = { script: parameters } + end + unless parameters.keys.length == 1 + raise "invalid 'installer' stanza: only one of #{VALID_KEYS.inspect} is permitted" + end + key = parameters.keys.first + raise "invalid 'installer' stanza key: '#{key.inspect}'" unless VALID_KEYS.include?(key) + writer_method = "#{key}=".to_sym + send(writer_method, parameters[key]) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/license.rb b/Library/Homebrew/cask/lib/hbc/dsl/license.rb new file mode 100644 index 0000000000..5f607c2685 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/license.rb @@ -0,0 +1,66 @@ +class Hbc::DSL::License + # a generic category can always be given as a license, so + # category names should be given as both key and value + VALID_LICENSES = { + # license category + unknown: :unknown, + + other: :other, + + closed: :closed, + commercial: :closed, + gratis: :closed, + freemium: :closed, + + oss: :oss, + affero: :oss, + apache: :oss, + arphic: :oss, + artistic: :oss, + bsd: :oss, + cc: :oss, + eclipse: :oss, + gpl: :oss, + isc: :oss, + lppl: :oss, + ncsa: :oss, + mit: :oss, + mpl: :oss, + ofl: :oss, + public_domain: :oss, + ubuntu_font: :oss, + x11: :oss, + }.freeze + + DEFAULT_LICENSE = :unknown + DEFAULT_CATEGORY = VALID_LICENSES[DEFAULT_LICENSE] + + attr_reader :value + + def self.check_constants + categories = Set.new(VALID_LICENSES.values) + categories.each do |cat| + next if VALID_LICENSES.key?(cat) + raise "license category is not a value: '#{@cat.inspect}'" + end + end + + def self.category(license) + VALID_LICENSES.fetch(license, DEFAULT_CATEGORY) + end + + def initialize(arg) + @value = arg + @value = DEFAULT_LICENSE if @value.nil? + return if VALID_LICENSES.key?(@value) + raise "invalid license value: '#{@value.inspect}'" + end + + def category + self.class.category(@value) + end + + def to_s + @value.inspect + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/postflight.rb b/Library/Homebrew/cask/lib/hbc/dsl/postflight.rb new file mode 100644 index 0000000000..321c7e81a4 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/postflight.rb @@ -0,0 +1,9 @@ +require "hbc/staged" + +class Hbc::DSL::Postflight < Hbc::DSL::Base + include Hbc::Staged + + def suppress_move_to_applications(options = {}) + # TODO: Remove from all casks because it is no longer needed + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/preflight.rb b/Library/Homebrew/cask/lib/hbc/dsl/preflight.rb new file mode 100644 index 0000000000..a0d53c69ce --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/preflight.rb @@ -0,0 +1,3 @@ +class Hbc::DSL::Preflight < Hbc::DSL::Base + include Hbc::Staged +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/stanza_proxy.rb b/Library/Homebrew/cask/lib/hbc/dsl/stanza_proxy.rb new file mode 100644 index 0000000000..02c76fb278 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/stanza_proxy.rb @@ -0,0 +1,37 @@ +class Hbc::DSL::StanzaProxy + attr_reader :type + + def self.once(type) + resolved = nil + new(type) { resolved ||= yield } + end + + def initialize(type, &resolver) + @type = type + @resolver = resolver + end + + def proxy? + true + end + + def to_s + @resolver.call.to_s + end + + # Serialization for dumpcask + def encode_with(coder) + coder["type"] = type + coder["resolved"] = @resolver.call + end + + def respond_to?(symbol, include_private = false) + return true if %i{encode_with proxy? to_s type}.include?(symbol) + return false if symbol == :to_ary + @resolver.call.respond_to?(symbol, include_private) + end + + def method_missing(symbol, *args) + @resolver.call.send(symbol, *args) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/uninstall_postflight.rb b/Library/Homebrew/cask/lib/hbc/dsl/uninstall_postflight.rb new file mode 100644 index 0000000000..bd8777ca72 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/uninstall_postflight.rb @@ -0,0 +1,2 @@ +class Hbc::DSL::UninstallPostflight < Hbc::DSL::Base +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/uninstall_preflight.rb b/Library/Homebrew/cask/lib/hbc/dsl/uninstall_preflight.rb new file mode 100644 index 0000000000..994151c259 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/uninstall_preflight.rb @@ -0,0 +1,5 @@ +require "hbc/staged" + +class Hbc::DSL::UninstallPreflight < Hbc::DSL::Base + include Hbc::Staged +end diff --git a/Library/Homebrew/cask/lib/hbc/dsl/version.rb b/Library/Homebrew/cask/lib/hbc/dsl/version.rb new file mode 100644 index 0000000000..e01e67ea29 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/dsl/version.rb @@ -0,0 +1,111 @@ +class Hbc::DSL::Version < ::String + DIVIDERS = { + "." => :dots, + "-" => :hyphens, + "_" => :underscores, + "/" => :slashes, + }.freeze + + DIVIDER_REGEX = %r{(#{DIVIDERS.keys.map { |v| Regexp.quote(v) }.join('|')})} + + MAJOR_MINOR_PATCH_REGEX = %r{^(\d+)(?:\.(\d+)(?:\.(\d+))?)?} + + class << self + private + + def define_divider_methods(divider) + define_divider_deletion_method(divider) + define_divider_conversion_methods(divider) + end + + def define_divider_deletion_method(divider) + method_name = deletion_method_name(divider) + define_method(method_name) do + version { delete(divider) } + end + end + + def deletion_method_name(divider) + "no_#{DIVIDERS[divider]}" + end + + def define_divider_conversion_methods(left_divider) + (DIVIDERS.keys - [left_divider]).each do |right_divider| + define_divider_conversion_method(left_divider, right_divider) + end + end + + def define_divider_conversion_method(left_divider, right_divider) + method_name = conversion_method_name(left_divider, right_divider) + define_method(method_name) do + version { gsub(left_divider, right_divider) } + end + end + + def conversion_method_name(left_divider, right_divider) + "#{DIVIDERS[left_divider]}_to_#{DIVIDERS[right_divider]}" + end + end + + DIVIDERS.keys.each do |divider| + define_divider_methods(divider) + end + + attr_reader :raw_version + + def initialize(raw_version) + @raw_version = raw_version + super(raw_version.to_s) + end + + def latest? + to_s == "latest" + end + + def major + version { slice(MAJOR_MINOR_PATCH_REGEX, 1) } + end + + def minor + version { slice(MAJOR_MINOR_PATCH_REGEX, 2) } + end + + def patch + version { slice(MAJOR_MINOR_PATCH_REGEX, 3) } + end + + def major_minor + version { [major, minor].reject(&:empty?).join(".") } + end + + def major_minor_patch + version { [major, minor, patch].reject(&:empty?).join(".") } + end + + def before_comma + version { split(",", 2)[0] } + end + + def after_comma + version { split(",", 2)[1] } + end + + def before_colon + version { split(":", 2)[0] } + end + + def after_colon + version { split(":", 2)[1] } + end + + def no_dividers + version { gsub(DIVIDER_REGEX, "") } + end + + private + + def version + return self if empty? || latest? + self.class.new(yield) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/exceptions.rb b/Library/Homebrew/cask/lib/hbc/exceptions.rb new file mode 100644 index 0000000000..8813aaedf8 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/exceptions.rb @@ -0,0 +1,146 @@ +class Hbc::CaskError < RuntimeError; end + +class Hbc::AbstractCaskErrorWithToken < Hbc::CaskError + attr_reader :token + + def initialize(token) + @token = token + end +end + +class Hbc::CaskNotInstalledError < Hbc::AbstractCaskErrorWithToken + def to_s + "#{token} is not installed" + end +end + +class Hbc::CaskUnavailableError < Hbc::AbstractCaskErrorWithToken + def to_s + "No available Cask for #{token}" + end +end + +class Hbc::CaskAlreadyCreatedError < Hbc::AbstractCaskErrorWithToken + def to_s + %Q{A Cask for #{token} already exists. Run "brew cask cat #{token}" to see it.} + end +end + +class Hbc::CaskAlreadyInstalledError < Hbc::AbstractCaskErrorWithToken + def to_s + %Q{A Cask for #{token} is already installed. Add the "--force" option to force re-install.} + end +end + +class Hbc::CaskAutoUpdatesError < Hbc::AbstractCaskErrorWithToken + def to_s + %Q{A Cask for #{token} is already installed and using auto-updates. Add the "--force" option to force re-install.} + end +end + +class Hbc::CaskCommandFailedError < Hbc::CaskError + def initialize(cmd, stdout, stderr, status) + @cmd = cmd + @stdout = stdout + @stderr = stderr + @status = status + end + + def to_s + <<-EOS +Command failed to execute! + +==> Failed command: +#{@cmd} + +==> Standard Output of failed command: +#{@stdout} + +==> Standard Error of failed command: +#{@stderr} + +==> Exit status of failed command: +#{@status.inspect} + EOS + end +end + +class Hbc::CaskX11DependencyError < Hbc::AbstractCaskErrorWithToken + def to_s + <<-EOS.undent + #{token} requires XQuartz/X11, which can be installed via homebrew-cask by + + brew cask install xquartz + + or manually, by downloading the package from + + https://www.xquartz.org/ + EOS + end +end + +class Hbc::CaskCyclicCaskDependencyError < Hbc::AbstractCaskErrorWithToken + def to_s + "Cask '#{token}' includes cyclic dependencies on other Casks and could not be installed." + end +end + +class Hbc::CaskUnspecifiedError < Hbc::CaskError + def to_s + "This command requires a Cask token" + end +end + +class Hbc::CaskInvalidError < Hbc::AbstractCaskErrorWithToken + attr_reader :submsg + def initialize(token, *submsg) + super(token) + @submsg = submsg.join(" ") + end + + def to_s + "Cask '#{token}' definition is invalid" + (!submsg.empty? ? ": #{submsg}" : "") + end +end + +class Hbc::CaskTokenDoesNotMatchError < Hbc::CaskInvalidError + def initialize(token, header_token) + super(token, "Bad header line: '#{header_token}' does not match file name") + end +end + +class Hbc::CaskSha256MissingError < ArgumentError +end + +class Hbc::CaskSha256MismatchError < RuntimeError + attr_reader :path, :expected, :actual + def initialize(path, expected, actual) + @path = path + @expected = expected + @actual = actual + end + + def to_s + <<-EOS.undent + sha256 mismatch + Expected: #{expected} + Actual: #{actual} + File: #{path} + To retry an incomplete download, remove the file above. + EOS + end +end + +class Hbc::CaskNoShasumError < Hbc::CaskError + attr_reader :token + def initialize(token) + @token = token + end + + def to_s + <<-EOS.undent + Cask '#{token}' does not have a sha256 checksum defined and was not installed. + This means you have the "--require-sha" option set, perhaps in your HOMEBREW_CASK_OPTS. + EOS + end +end diff --git a/Library/Homebrew/cask/lib/hbc/extend.rb b/Library/Homebrew/cask/lib/hbc/extend.rb new file mode 100644 index 0000000000..629c53468e --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/extend.rb @@ -0,0 +1,6 @@ +# monkeypatching +require "hbc/extend/hash" +require "hbc/extend/io" +require "hbc/extend/optparse" +require "hbc/extend/pathname" +require "hbc/extend/string" diff --git a/Library/Homebrew/cask/lib/hbc/extend/hash.rb b/Library/Homebrew/cask/lib/hbc/extend/hash.rb new file mode 100644 index 0000000000..dc28cfb29d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/extend/hash.rb @@ -0,0 +1,7 @@ +class Hash + def assert_valid_keys(*valid_keys) + unknown_keys = keys - valid_keys + return if unknown_keys.empty? + raise Hbc::CaskError, %Q{Unknown keys: #{unknown_keys.inspect}. Running "#{UPDATE_CMD}" will likely fix it.} + end +end diff --git a/Library/Homebrew/cask/lib/hbc/extend/io.rb b/Library/Homebrew/cask/lib/hbc/extend/io.rb new file mode 100644 index 0000000000..1357293cdc --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/extend/io.rb @@ -0,0 +1,10 @@ +class IO + def readline_nonblock(sep = $INPUT_RECORD_SEPARATOR) + buffer = "" + buffer.concat(read_nonblock(1)) while buffer[-1] != sep + buffer + rescue IO::WaitReadable, EOFError => e + raise e if buffer.empty? + buffer + end +end diff --git a/Library/Homebrew/cask/lib/hbc/extend/optparse.rb b/Library/Homebrew/cask/lib/hbc/extend/optparse.rb new file mode 100644 index 0000000000..784d6d699d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/extend/optparse.rb @@ -0,0 +1,6 @@ +require "optparse" +require "pathname" + +OptionParser.accept Pathname do |path| + Pathname(path).expand_path if path +end diff --git a/Library/Homebrew/cask/lib/hbc/extend/pathname.rb b/Library/Homebrew/cask/lib/hbc/extend/pathname.rb new file mode 100644 index 0000000000..598a99cd2d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/extend/pathname.rb @@ -0,0 +1,19 @@ +require "pathname" + +class Pathname + # extended to support common double extensions + def extname(path = to_s) + %r{(\.(dmg|tar|cpio|pax)\.(gz|bz2|lz|xz|Z|zip))$} =~ path + return Regexp.last_match(1) if Regexp.last_match(1) + File.extname(path) + end + + # https://bugs.ruby-lang.org/issues/9915 + if RUBY_VERSION == "2.0.0" + prepend Module.new { + def inspect + super.force_encoding(@path.encoding) + end + } + end +end diff --git a/Library/Homebrew/cask/lib/hbc/extend/string.rb b/Library/Homebrew/cask/lib/hbc/extend/string.rb new file mode 100644 index 0000000000..38c284194b --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/extend/string.rb @@ -0,0 +1,5 @@ +class String + def undent + gsub(%r{^.{#{(slice(%r{^ +}) || '').length}}}, "") + end +end diff --git a/Library/Homebrew/cask/lib/hbc/fetcher.rb b/Library/Homebrew/cask/lib/hbc/fetcher.rb new file mode 100644 index 0000000000..44a898ce0a --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/fetcher.rb @@ -0,0 +1,22 @@ +require "open3" + +class Hbc::Fetcher + TIMEOUT = 10 + + def self.head(url) + if url.to_s =~ %r{googlecode} + googlecode_fake_head(url) + else + Hbc::SystemCommand.run("/usr/bin/curl", + args: ["--max-time", TIMEOUT, "--silent", "--location", "--head", url]).stdout + end + end + + # google code does not properly respond to HTTP HEAD requests, like a jerk + # this fakes a HEAD by doing a GET, taking the first 20 lines, then running away + def self.googlecode_fake_head(url) + command = "curl --max-time #{TIMEOUT} --verbose --location '#{url}' | head -n 20 > /dev/null" + stderr = Open3.capture3(command)[1] + stderr.split("\n").grep(%r{^< }).map { |line| line.sub(%r{^< }, "") }.join("\n") + end +end diff --git a/Library/Homebrew/cask/lib/hbc/installer.rb b/Library/Homebrew/cask/lib/hbc/installer.rb new file mode 100644 index 0000000000..8e55b8a992 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/installer.rb @@ -0,0 +1,343 @@ +require "rubygems" + +require "extend/pathname" +require "hbc/cask_dependencies" +require "hbc/staged" +require "hbc/verify" + +class Hbc::Installer + # TODO: it is unwise for Hbc::Staged to be a module, when we are + # dealing with both staged and unstaged Casks here. This should + # either be a class which is only sometimes instantiated, or there + # should be explicit checks on whether staged state is valid in + # every method. + include Hbc::Staged + include Hbc::Verify + + attr_reader :force, :skip_cask_deps + + PERSISTENT_METADATA_SUBDIRS = ["gpg"].freeze + + def initialize(cask, command: Hbc::SystemCommand, force: false, skip_cask_deps: false, require_sha: false) + @cask = cask + @command = command + @force = force + @skip_cask_deps = skip_cask_deps + @require_sha = require_sha + end + + def self.print_caveats(cask) + odebug "Printing caveats" + unless cask.caveats.empty? + output = capture_output do + cask.caveats.each do |caveat| + if caveat.respond_to?(:eval_and_print) + caveat.eval_and_print(cask) + else + puts caveat + end + end + end + + unless output.empty? + ohai "Caveats" + puts output + end + end + end + + def self.capture_output(&block) + old_stdout = $stdout + $stdout = Buffer.new($stdout.tty?) + block.call + output = $stdout.string + $stdout = old_stdout + output + end + + def install + odebug "Hbc::Installer.install" + + if @cask.installed? && @cask.auto_updates && !force + raise Hbc::CaskAutoUpdatesError, @cask + end + + raise Hbc::CaskAlreadyInstalledError, @cask if @cask.installed? && !force + + print_caveats + + begin + satisfy_dependencies + verify_has_sha if @require_sha && !@force + download + verify + extract_primary_container + install_artifacts + save_caskfile + enable_accessibility_access + rescue StandardError => e + purge_versioned_files + raise e + end + + puts summary + end + + def summary + s = if MacOS.version >= :lion && !ENV["HOMEBREW_NO_EMOJI"] + (ENV["HOMEBREW_INSTALL_BADGE"] || "\xf0\x9f\x8d\xba") + " " + else + "#{Hbc::Utils::Tty.blue.bold}==>#{Hbc::Utils::Tty.reset.bold} Success!#{Hbc::Utils::Tty.reset} " + end + s << "#{@cask} was successfully installed!" + end + + def download + odebug "Downloading" + download = Hbc::Download.new(@cask, force: false) + @downloaded_path = download.perform + odebug "Downloaded to -> #{@downloaded_path}" + @downloaded_path + end + + def verify_has_sha + odebug "Checking cask has checksum" + return unless @cask.sha256 == :no_check + raise Hbc::CaskNoShasumError, @cask + end + + def verify + Hbc::Verify.all(@cask, @downloaded_path) + end + + def extract_primary_container + odebug "Extracting primary container" + FileUtils.mkdir_p @cask.staged_path + container = if @cask.container && @cask.container.type + Hbc::Container.from_type(@cask.container.type) + else + Hbc::Container.for_path(@downloaded_path, @command) + end + unless container + raise Hbc::CaskError, "Uh oh, could not figure out how to unpack '#{@downloaded_path}'" + end + odebug "Using container class #{container} for #{@downloaded_path}" + container.new(@cask, @downloaded_path, @command).extract + end + + def install_artifacts + odebug "Installing artifacts" + artifacts = Hbc::Artifact.for_cask(@cask) + odebug "#{artifacts.length} artifact/s defined", artifacts + artifacts.each do |artifact| + odebug "Installing artifact of class #{artifact}" + options = { command: @command, force: force } + artifact.new(@cask, options).install_phase + end + end + + # TODO: move dependencies to a separate class + # dependencies should also apply for "brew cask stage" + # override dependencies with --force or perhaps --force-deps + def satisfy_dependencies + if @cask.depends_on + ohai "Satisfying dependencies" + macos_dependencies + arch_dependencies + x11_dependencies + formula_dependencies + cask_dependencies unless skip_cask_deps + puts "complete" + end + end + + def macos_dependencies + return unless @cask.depends_on.macos + if @cask.depends_on.macos.first.is_a?(Array) + operator, release = @cask.depends_on.macos.first + unless MacOS.version.send(operator, release) + raise Hbc::CaskError, "Cask #{@cask} depends on macOS release #{operator} #{release}, but you are running release #{MacOS.version}." + end + elsif @cask.depends_on.macos.length > 1 + unless @cask.depends_on.macos.include?(Gem::Version.new(MacOS.version.to_s)) + raise Hbc::CaskError, "Cask #{@cask} depends on macOS release being one of [#{@cask.depends_on.macos.map(&:to_s).join(', ')}], but you are running release #{MacOS.version}." + end + else + unless MacOS.version == @cask.depends_on.macos.first + raise Hbc::CaskError, "Cask #{@cask} depends on macOS release #{@cask.depends_on.macos.first}, but you are running release #{MacOS.version}." + end + end + end + + def arch_dependencies + return if @cask.depends_on.arch.nil? + @current_arch ||= { type: Hardware::CPU.type, bits: Hardware::CPU.bits } + return if @cask.depends_on.arch.any? { |arch| + arch[:type] == @current_arch[:type] && + Array(arch[:bits]).include?(@current_arch[:bits]) + } + raise Hbc::CaskError, "Cask #{@cask} depends on hardware architecture being one of [#{@cask.depends_on.arch.map(&:to_s).join(', ')}], but you are running #{@current_arch}" + end + + def x11_dependencies + return unless @cask.depends_on.x11 + raise Hbc::CaskX11DependencyError, @cask.token if Hbc.x11_libpng.select(&:exist?).empty? + end + + def formula_dependencies + return unless @cask.depends_on.formula && !@cask.depends_on.formula.empty? + ohai "Installing Formula dependencies from Homebrew" + @cask.depends_on.formula.each do |dep_name| + print "#{dep_name} ... " + installed = @command.run(Hbc.homebrew_executable, + args: ["list", "--versions", dep_name], + print_stderr: false).stdout.include?(dep_name) + if installed + puts "already installed" + else + @command.run!(Hbc.homebrew_executable, + args: ["install", dep_name]) + puts "done" + end + end + end + + def cask_dependencies + return unless @cask.depends_on.cask && !@cask.depends_on.cask.empty? + ohai "Installing Cask dependencies: #{@cask.depends_on.cask.join(', ')}" + deps = Hbc::CaskDependencies.new(@cask) + deps.sorted.each do |dep_token| + puts "#{dep_token} ..." + dep = Hbc.load(dep_token) + if dep.installed? + puts "already installed" + else + Hbc::Installer.new(dep, force: false, skip_cask_deps: true).install + puts "done" + end + end + end + + def print_caveats + self.class.print_caveats(@cask) + end + + # TODO: logically could be in a separate class + def enable_accessibility_access + return unless @cask.accessibility_access + ohai "Enabling accessibility access" + if MacOS.version <= :mountain_lion + @command.run!("/usr/bin/touch", + args: [Hbc.pre_mavericks_accessibility_dotfile], + sudo: true) + elsif MacOS.version <= :yosemite + @command.run!("/usr/bin/sqlite3", + args: [ + Hbc.tcc_db, + "INSERT OR REPLACE INTO access VALUES('kTCCServiceAccessibility','#{bundle_identifier}',0,1,1,NULL);", + ], + sudo: true) + else + @command.run!("/usr/bin/sqlite3", + args: [ + Hbc.tcc_db, + "INSERT OR REPLACE INTO access VALUES('kTCCServiceAccessibility','#{bundle_identifier}',0,1,1,NULL,NULL);", + ], + sudo: true) + end + end + + def disable_accessibility_access + return unless @cask.accessibility_access + if MacOS.version >= :mavericks + ohai "Disabling accessibility access" + @command.run!("/usr/bin/sqlite3", + args: [ + Hbc.tcc_db, + "DELETE FROM access WHERE client='#{bundle_identifier}';", + ], + sudo: true) + else + opoo <<-EOS.undent + Accessibility access was enabled for #{@cask}, but it is not safe to disable + automatically on this version of macOS. See System Preferences. + EOS + end + end + + def save_caskfile + timestamp = :now + create = true + savedir = @cask.metadata_subdir("Casks", timestamp, create) + if Dir.entries(savedir).size > 2 + # should not happen + raise Hbc::CaskAlreadyInstalledError, @cask unless force + savedir.rmtree + FileUtils.mkdir_p savedir + end + FileUtils.copy(@cask.sourcefile_path, savedir) if @cask.sourcefile_path + end + + def uninstall + odebug "Hbc::Installer.uninstall" + disable_accessibility_access + uninstall_artifacts + purge_versioned_files + purge_caskroom_path if force + end + + def uninstall_artifacts + odebug "Un-installing artifacts" + artifacts = Hbc::Artifact.for_cask(@cask) + odebug "#{artifacts.length} artifact/s defined", artifacts + artifacts.each do |artifact| + odebug "Un-installing artifact of class #{artifact}" + options = { command: @command, force: force } + artifact.new(@cask, options).uninstall_phase + end + end + + def zap + ohai %Q{Implied "brew cask uninstall #{@cask}"} + uninstall_artifacts + if Hbc::Artifact::Zap.me?(@cask) + ohai "Dispatching zap stanza" + Hbc::Artifact::Zap.new(@cask, command: @command).zap_phase + else + opoo "No zap stanza present for Cask '#{@cask}'" + end + ohai "Removing all staged versions of Cask '#{@cask}'" + purge_caskroom_path + end + + def gain_permissions_remove(path) + Hbc::Utils.gain_permissions_remove(path, command: @command) + end + + def purge_versioned_files + odebug "Purging files for version #{@cask.version} of Cask #{@cask}" + + # versioned staged distribution + gain_permissions_remove(@cask.staged_path) if !@cask.staged_path.nil? && @cask.staged_path.exist? + + # Homebrew-Cask metadata + if @cask.metadata_versioned_container_path.respond_to?(:children) && + @cask.metadata_versioned_container_path.exist? + @cask.metadata_versioned_container_path.children.each do |subdir| + unless PERSISTENT_METADATA_SUBDIRS.include?(subdir.basename) + gain_permissions_remove(subdir) + end + end + end + @cask.metadata_versioned_container_path.rmdir_if_possible + @cask.metadata_master_container_path.rmdir_if_possible + + # toplevel staged distribution + @cask.caskroom_path.rmdir_if_possible + end + + def purge_caskroom_path + odebug "Purging all staged versions of Cask #{@cask}" + gain_permissions_remove(@cask.caskroom_path) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/locations.rb b/Library/Homebrew/cask/lib/hbc/locations.rb new file mode 100644 index 0000000000..e4d88f3187 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/locations.rb @@ -0,0 +1,196 @@ +module Hbc::Locations + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def legacy_caskroom + @legacy_caskroom ||= Pathname.new("/opt/homebrew-cask/Caskroom") + end + + def default_caskroom + @default_caskroom ||= homebrew_repository.join("Caskroom") + end + + def caskroom + @caskroom ||= begin + if Hbc::Utils.path_occupied?(legacy_caskroom) + opoo <<-EOS.undent + The default Caskroom location has moved to #{default_caskroom}. + + Please migrate your Casks to the new location and delete #{legacy_caskroom}, + or if you would like to keep your Caskroom at #{legacy_caskroom}, add the + following to your HOMEBREW_CASK_OPTS: + + --caskroom=#{legacy_caskroom} + + For more details on each of those options, see https://github.com/caskroom/homebrew-cask/issues/21913. + EOS + legacy_caskroom + else + default_caskroom + end + end + end + + def caskroom=(caskroom) + @caskroom = caskroom + end + + def legacy_cache + @legacy_cache ||= homebrew_cache.join("Casks") + end + + def cache + @cache ||= homebrew_cache.join("Cask") + end + + attr_writer :appdir + + def appdir + @appdir ||= Pathname.new("/Applications").expand_path + end + + attr_writer :prefpanedir + + def prefpanedir + @prefpanedir ||= Pathname.new("~/Library/PreferencePanes").expand_path + end + + attr_writer :qlplugindir + + def qlplugindir + @qlplugindir ||= Pathname.new("~/Library/QuickLook").expand_path + end + + attr_writer :fontdir + + def fontdir + @fontdir ||= Pathname.new("~/Library/Fonts").expand_path + end + + attr_writer :colorpickerdir + + def colorpickerdir + @colorpickerdir ||= Pathname.new("~/Library/ColorPickers").expand_path + end + + attr_writer :servicedir + + def servicedir + @servicedir ||= Pathname.new("~/Library/Services").expand_path + end + + attr_writer :binarydir + + def binarydir + @binarydir ||= homebrew_prefix.join("bin") + end + + attr_writer :input_methoddir + + def input_methoddir + @input_methoddir ||= Pathname.new("~/Library/Input Methods").expand_path + end + + attr_writer :internet_plugindir + + def internet_plugindir + @internet_plugindir ||= Pathname.new("~/Library/Internet Plug-Ins").expand_path + end + + attr_writer :audio_unit_plugindir + + def audio_unit_plugindir + @audio_unit_plugindir ||= Pathname.new("~/Library/Audio/Plug-Ins/Components").expand_path + end + + attr_writer :vst_plugindir + + def vst_plugindir + @vst_plugindir ||= Pathname.new("~/Library/Audio/Plug-Ins/VST").expand_path + end + + attr_writer :vst3_plugindir + + def vst3_plugindir + @vst3_plugindir ||= Pathname.new("~/Library/Audio/Plug-Ins/VST3").expand_path + end + + attr_writer :screen_saverdir + + def screen_saverdir + @screen_saverdir ||= Pathname.new("~/Library/Screen Savers").expand_path + end + + attr_writer :default_tap + + def default_tap + @default_tap ||= Tap.fetch("caskroom/homebrew-cask") + end + + def path(query) + query = query.sub(%r{\.rb$}i, "") + token_with_tap = if query.include?("/") + query + else + all_tokens.detect do |tap_and_token| + tap_and_token.split("/")[2] == query + end + end + + if token_with_tap + user, repo, token = token_with_tap.split("/") + Tap.fetch(user, repo).cask_dir.join("#{token}.rb") + else + default_tap.cask_dir.join("#{query}.rb") + end + end + + def tcc_db + @tcc_db ||= Pathname.new("/Library/Application Support/com.apple.TCC/TCC.db") + end + + def pre_mavericks_accessibility_dotfile + @pre_mavericks_accessibility_dotfile ||= Pathname.new("/private/var/db/.AccessibilityAPIEnabled") + end + + def x11_executable + @x11_executable ||= Pathname.new("/usr/X11/bin/X") + end + + def x11_libpng + @x11_libpng ||= [Pathname.new("/opt/X11/lib/libpng.dylib"), Pathname.new("/usr/X11/lib/libpng.dylib")] + end + + def homebrew_cache + @homebrew_cache ||= HOMEBREW_CACHE + end + + def homebrew_cache=(path) + @homebrew_cache = path ? Pathname.new(path) : path + end + + def homebrew_executable + @homebrew_executable ||= HOMEBREW_BREW_FILE + end + + def homebrew_prefix + # where Homebrew links + @homebrew_prefix ||= HOMEBREW_PREFIX + end + + def homebrew_prefix=(path) + @homebrew_prefix = path ? Pathname.new(path) : path + end + + def homebrew_repository + # where Homebrew's .git dir is found + @homebrew_repository ||= HOMEBREW_REPOSITORY + end + + def homebrew_repository=(path) + @homebrew_repository = path ? Pathname.new(path) : path + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/macos.rb b/Library/Homebrew/cask/lib/hbc/macos.rb new file mode 100644 index 0000000000..5e25c657ed --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/macos.rb @@ -0,0 +1,378 @@ +require "set" + +require "os/mac/version" + +module OS::Mac + SYSTEM_DIRS = [ + "/", + "/Applications", + "/Applications/Utilities", + "/Incompatible Software", + "/Library", + "/Library/Application Support", + "/Library/Audio", + "/Library/Caches", + "/Library/ColorPickers", + "/Library/ColorSync", + "/Library/Components", + "/Library/Compositions", + "/Library/Contextual Menu Items", + "/Library/CoreMediaIO", + "/Library/Desktop Pictures", + "/Library/Developer", + "/Library/Dictionaries", + "/Library/DirectoryServices", + "/Library/Documentation", + "/Library/Extensions", + "/Library/Filesystems", + "/Library/Fonts", + "/Library/Frameworks", + "/Library/Graphics", + "/Library/Image Capture", + "/Library/Input Methods", + "/Library/Internet Plug-Ins", + "/Library/Java", + "/Library/Keyboard Layouts", + "/Library/Keychains", + "/Library/LaunchAgents", + "/Library/LaunchDaemons", + "/Library/Logs", + "/Library/Messages", + "/Library/Modem Scripts", + "/Library/OpenDirectory", + "/Library/PDF Services", + "/Library/Perl", + "/Library/PreferencePanes", + "/Library/Preferences", + "/Library/Printers", + "/Library/PrivilegedHelperTools", + "/Library/Python", + "/Library/QuickLook", + "/Library/QuickTime", + "/Library/Receipts", + "/Library/Ruby", + "/Library/Sandbox", + "/Library/Screen Savers", + "/Library/ScriptingAdditions", + "/Library/Scripts", + "/Library/Security", + "/Library/Speech", + "/Library/Spelling", + "/Library/Spotlight", + "/Library/StartupItems", + "/Library/SystemProfiler", + "/Library/Updates", + "/Library/User Pictures", + "/Library/Video", + "/Library/WebServer", + "/Library/Widgets", + "/Library/iTunes", + "/Network", + "/System", + "/System/Library", + "/System/Library/Accessibility", + "/System/Library/Accounts", + "/System/Library/Address Book Plug-Ins", + "/System/Library/Assistant", + "/System/Library/Automator", + "/System/Library/BridgeSupport", + "/System/Library/Caches", + "/System/Library/ColorPickers", + "/System/Library/ColorSync", + "/System/Library/Colors", + "/System/Library/Components", + "/System/Library/Compositions", + "/System/Library/CoreServices", + "/System/Library/DTDs", + "/System/Library/DirectoryServices", + "/System/Library/Displays", + "/System/Library/Extensions", + "/System/Library/Filesystems", + "/System/Library/Filters", + "/System/Library/Fonts", + "/System/Library/Frameworks", + "/System/Library/Graphics", + "/System/Library/IdentityServices", + "/System/Library/Image Capture", + "/System/Library/Input Methods", + "/System/Library/InternetAccounts", + "/System/Library/Java", + "/System/Library/KerberosPlugins", + "/System/Library/Keyboard Layouts", + "/System/Library/Keychains", + "/System/Library/LaunchAgents", + "/System/Library/LaunchDaemons", + "/System/Library/LinguisticData", + "/System/Library/LocationBundles", + "/System/Library/LoginPlugins", + "/System/Library/Messages", + "/System/Library/Metadata", + "/System/Library/MonitorPanels", + "/System/Library/OpenDirectory", + "/System/Library/OpenSSL", + "/System/Library/Password Server Filters", + "/System/Library/PerformanceMetrics", + "/System/Library/Perl", + "/System/Library/PreferencePanes", + "/System/Library/Printers", + "/System/Library/PrivateFrameworks", + "/System/Library/QuickLook", + "/System/Library/QuickTime", + "/System/Library/QuickTimeJava", + "/System/Library/Recents", + "/System/Library/SDKSettingsPlist", + "/System/Library/Sandbox", + "/System/Library/Screen Savers", + "/System/Library/ScreenReader", + "/System/Library/ScriptingAdditions", + "/System/Library/ScriptingDefinitions", + "/System/Library/Security", + "/System/Library/Services", + "/System/Library/Sounds", + "/System/Library/Speech", + "/System/Library/Spelling", + "/System/Library/Spotlight", + "/System/Library/StartupItems", + "/System/Library/SyncServices", + "/System/Library/SystemConfiguration", + "/System/Library/SystemProfiler", + "/System/Library/Tcl", + "/System/Library/TextEncodings", + "/System/Library/User Template", + "/System/Library/UserEventPlugins", + "/System/Library/Video", + "/System/Library/WidgetResources", + "/User Information", + "/Users", + "/Volumes", + "/bin", + "/boot", + "/cores", + "/dev", + "/etc", + "/etc/X11", + "/etc/opt", + "/etc/sgml", + "/etc/xml", + "/home", + "/libexec", + "/lost+found", + "/media", + "/mnt", + "/net", + "/opt", + "/private", + "/private/etc", + "/private/tftpboot", + "/private/tmp", + "/private/var", + "/proc", + "/root", + "/sbin", + "/srv", + "/tmp", + "/usr", + "/usr/X11R6", + "/usr/bin", + "/usr/etc", + "/usr/include", + "/usr/lib", + "/usr/libexec", + "/usr/local", + "/usr/local/Cellar", + "/usr/local/Frameworks", + "/usr/local/Library", + "/usr/local/bin", + "/usr/local/etc", + "/usr/local/include", + "/usr/local/lib", + "/usr/local/libexec", + "/usr/local/opt", + "/usr/local/share", + "/usr/local/share/man", + "/usr/local/share/man/man1", + "/usr/local/share/man/man2", + "/usr/local/share/man/man3", + "/usr/local/share/man/man4", + "/usr/local/share/man/man5", + "/usr/local/share/man/man6", + "/usr/local/share/man/man7", + "/usr/local/share/man/man8", + "/usr/local/share/man/man9", + "/usr/local/share/man/mann", + "/usr/local/var", + "/usr/local/var/lib", + "/usr/local/var/lock", + "/usr/local/var/run", + "/usr/sbin", + "/usr/share", + "/usr/share/man", + "/usr/share/man/man1", + "/usr/share/man/man2", + "/usr/share/man/man3", + "/usr/share/man/man4", + "/usr/share/man/man5", + "/usr/share/man/man6", + "/usr/share/man/man7", + "/usr/share/man/man8", + "/usr/share/man/man9", + "/usr/share/man/mann", + "/usr/src", + "/var", + "/var/cache", + "/var/lib", + "/var/lock", + "/var/log", + "/var/mail", + "/var/run", + "/var/spool", + "/var/spool/mail", + "/var/tmp", + ] + .map(&method(:Pathname)) + .to_set + .freeze + + # TODO: There should be a way to specify a containing + # directory under which nothing can be deleted. + UNDELETABLE_DIRS = [ + "~/", + "~/Applications", + "~/Desktop", + "~/Documents", + "~/Downloads", + "~/Mail", + "~/Movies", + "~/Music", + "~/Music/iTunes", + "~/Music/iTunes/iTunes Music", + "~/Music/iTunes/Album Artwork", + "~/News", + "~/Pictures", + "~/Pictures/Desktops", + "~/Pictures/Photo Booth", + "~/Pictures/iChat Icons", + "~/Pictures/iPhoto Library", + "~/Public", + "~/Sites", + "~/Library", + "~/Library/.localized", + "~/Library/Accessibility", + "~/Library/Accounts", + "~/Library/Address Book Plug-Ins", + "~/Library/Application Scripts", + "~/Library/Application Support", + "~/Library/Application Support/Apple", + "~/Library/Application Support/com.apple.AssistiveControl", + "~/Library/Application Support/com.apple.QuickLook", + "~/Library/Application Support/com.apple.TCC", + "~/Library/Assistants", + "~/Library/Audio", + "~/Library/Automator", + "~/Library/Autosave Information", + "~/Library/Caches", + "~/Library/Calendars", + "~/Library/ColorPickers", + "~/Library/ColorSync", + "~/Library/Colors", + "~/Library/Components", + "~/Library/Compositions", + "~/Library/Containers", + "~/Library/Contextual Menu Items", + "~/Library/Cookies", + "~/Library/DTDs", + "~/Library/Desktop Pictures", + "~/Library/Developer", + "~/Library/Dictionaries", + "~/Library/DirectoryServices", + "~/Library/Displays", + "~/Library/Documentation", + "~/Library/Extensions", + "~/Library/Favorites", + "~/Library/FileSync", + "~/Library/Filesystems", + "~/Library/Filters", + "~/Library/FontCollections", + "~/Library/Fonts", + "~/Library/Frameworks", + "~/Library/GameKit", + "~/Library/Graphics", + "~/Library/Group Containers", + "~/Library/Icons", + "~/Library/IdentityServices", + "~/Library/Image Capture", + "~/Library/Images", + "~/Library/Input Methods", + "~/Library/Internet Plug-Ins", + "~/Library/InternetAccounts", + "~/Library/iTunes", + "~/Library/KeyBindings", + "~/Library/Keyboard Layouts", + "~/Library/Keychains", + "~/Library/LaunchAgents", + "~/Library/LaunchDaemons", + "~/Library/LocationBundles", + "~/Library/LoginPlugins", + "~/Library/Logs", + "~/Library/Mail", + "~/Library/Mail Downloads", + "~/Library/Messages", + "~/Library/Metadata", + "~/Library/Mobile Documents", + "~/Library/MonitorPanels", + "~/Library/OpenDirectory", + "~/Library/PDF Services", + "~/Library/PhonePlugins", + "~/Library/Phones", + "~/Library/PreferencePanes", + "~/Library/Preferences", + "~/Library/Printers", + "~/Library/PrivateFrameworks", + "~/Library/PubSub", + "~/Library/QuickLook", + "~/Library/QuickTime", + "~/Library/Receipts", + "~/Library/Recent Servers", + "~/Library/Recents", + "~/Library/Safari", + "~/Library/Saved Application State", + "~/Library/Screen Savers", + "~/Library/ScreenReader", + "~/Library/ScriptingAdditions", + "~/Library/ScriptingDefinitions", + "~/Library/Scripts", + "~/Library/Security", + "~/Library/Services", + "~/Library/Sounds", + "~/Library/Speech", + "~/Library/Spelling", + "~/Library/Spotlight", + "~/Library/StartupItems", + "~/Library/StickiesDatabase", + "~/Library/Sync Services", + "~/Library/SyncServices", + "~/Library/SyncedPreferences", + "~/Library/TextEncodings", + "~/Library/User Pictures", + "~/Library/Video", + "~/Library/Voices", + "~/Library/WebKit", + "~/Library/WidgetResources", + "~/Library/Widgets", + "~/Library/Workflows", + ] + .map { |x| Pathname(x.sub(%r{^~(?=(/|$))}, Dir.home)).expand_path } + .to_set + .union(SYSTEM_DIRS) + .freeze + + def system_dir?(dir) + SYSTEM_DIRS.include?(Pathname.new(dir).expand_path) + end + + def undeletable?(dir) + UNDELETABLE_DIRS.include?(Pathname.new(dir).expand_path) + end + + alias release version +end diff --git a/Library/Homebrew/cask/lib/hbc/options.rb b/Library/Homebrew/cask/lib/hbc/options.rb new file mode 100644 index 0000000000..c0e3e2ed0d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/options.rb @@ -0,0 +1,37 @@ +module Hbc::Options + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + attr_writer :no_binaries + + def no_binaries + @no_binaries ||= false + end + + attr_writer :debug + + def debug + @debug ||= false + end + + attr_writer :verbose + + def verbose + @verbose ||= false + end + + attr_writer :cleanup_outdated + + def cleanup_outdated + @cleanup_outdated ||= false + end + + attr_writer :help + + def help + @help ||= false + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/pkg.rb b/Library/Homebrew/cask/lib/hbc/pkg.rb new file mode 100644 index 0000000000..6f8d28c246 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/pkg.rb @@ -0,0 +1,113 @@ +class Hbc::Pkg + def self.all_matching(regexp, command) + command.run("/usr/sbin/pkgutil", args: ["--pkgs=#{regexp}"]).stdout.split("\n").map { |package_id| + new(package_id.chomp, command) + } + end + + attr_reader :package_id + + def initialize(package_id, command = Hbc::SystemCommand) + @package_id = package_id + @command = command + end + + def uninstall + odebug "Deleting pkg files" + pkgutil_bom_files.each_slice(500) do |file_slice| + @command.run("/bin/rm", args: file_slice.unshift("-f", "--"), sudo: true) + end + odebug "Deleting pkg symlinks and special files" + pkgutil_bom_specials.each_slice(500) do |file_slice| + @command.run("/bin/rm", args: file_slice.unshift("-f", "--"), sudo: true) + end + odebug "Deleting pkg directories" + _deepest_path_first(pkgutil_bom_dirs).each do |dir| + next unless dir.exist? && !MacOS.undeletable?(dir) + _with_full_permissions(dir) do + _clean_broken_symlinks(dir) + _clean_ds_store(dir) + _rmdir(dir) + end + end + forget + end + + def forget + odebug "Unregistering pkg receipt (aka forgetting)" + @command.run!("/usr/sbin/pkgutil", args: ["--forget", package_id], sudo: true) + end + + def pkgutil_bom(*type) + @command.run!("/usr/sbin/pkgutil", args: [*type, "--files", package_id].compact) + .stdout + .split("\n") + .map { |path| root.join(path) } + end + + def pkgutil_bom_files + @pkgutil_bom_files ||= pkgutil_bom("--only-files") + end + + def pkgutil_bom_dirs + @pkgutil_bom_dirs ||= pkgutil_bom("--only-dirs") + end + + def pkgutil_bom_all + @pkgutil_bom_all ||= pkgutil_bom + end + + def pkgutil_bom_specials + pkgutil_bom_all - pkgutil_bom_files - pkgutil_bom_dirs + end + + def root + @root ||= Pathname(info.fetch("volume")).join(info.fetch("install-location")) + end + + def info + @command.run!("/usr/sbin/pkgutil", args: ["--pkg-info-plist", package_id]) + .plist + end + + def _rmdir(path) + @command.run!("/bin/rmdir", args: ["--", path], sudo: true) if path.children.empty? + end + + def _with_full_permissions(path) + original_mode = (path.stat.mode % 0o1000).to_s(8) + # TODO: similarly read and restore macOS flags (cf man chflags) + @command.run!("/bin/chmod", args: ["--", "777", path], sudo: true) + yield + ensure + if path.exist? # block may have removed dir + @command.run!("/bin/chmod", args: ["--", original_mode, path], sudo: true) + end + end + + def _deepest_path_first(paths) + paths.sort do |path_a, path_b| + path_b.to_s.split("/").count <=> path_a.to_s.split("/").count + end + end + + # Some pkgs leave broken symlinks hanging around; we clean them out before + # attempting to rmdir to prevent extra cruft from lying around after + # uninstall + def _clean_broken_symlinks(dir) + dir.children.each do |child| + if _broken_symlink?(child) + @command.run!("/bin/rm", args: ["--", child], sudo: true) + end + end + end + + def _clean_ds_store(dir) + ds_store = dir.join(".DS_Store") + @command.run!("/bin/rm", args: ["--", ds_store], sudo: true) if ds_store.exist? + end + + def _broken_symlink?(path) + path.symlink? && !path.exist? + end +end diff --git a/Library/Homebrew/cask/lib/hbc/qualified_token.rb b/Library/Homebrew/cask/lib/hbc/qualified_token.rb new file mode 100644 index 0000000000..635e1cb3d6 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/qualified_token.rb @@ -0,0 +1,37 @@ +module Hbc::QualifiedToken + REPO_PREFIX = "homebrew-".freeze + + # per https://github.com/Homebrew/homebrew/blob/4c7bc9ec3bca729c898ee347b6135ba692ee0274/Library/Homebrew/cmd/tap.rb#L121 + USER_REGEX = %r{[a-z_\-]+} + + # per https://github.com/Homebrew/homebrew/blob/4c7bc9ec3bca729c898ee347b6135ba692ee0274/Library/Homebrew/cmd/tap.rb#L121 + REPO_REGEX = %r{(?:#{REPO_PREFIX})?\w+} + + # per https://github.com/caskroom/homebrew-cask/blob/master/CONTRIBUTING.md#generating-a-token-for-the-cask + TOKEN_REGEX = %r{[a-z0-9\-]+} + + TAP_REGEX = %r{#{USER_REGEX}[/\-]#{REPO_REGEX}} + + QUALIFIED_TOKEN_REGEX ||= %r{#{TAP_REGEX}/#{TOKEN_REGEX}} + + def self.parse(arg) + return nil unless arg.is_a?(String) && arg.downcase =~ %r{^#{QUALIFIED_TOKEN_REGEX}$} + path_elements = arg.downcase.split("/") + if path_elements.count == 2 + # eg phinze-cask/google-chrome. + # Not certain this form is needed, but it was supported in the past. + token = path_elements[1] + dash_elements = path_elements[0].split("-") + repo = dash_elements.pop + dash_elements.pop if dash_elements.count > 1 && dash_elements[-1] + "-" == REPO_PREFIX + user = dash_elements.join("-") + else + # eg caskroom/cask/google-chrome + # per https://github.com/Homebrew/homebrew/wiki/brew-tap + user, repo, token = path_elements + end + repo.sub!(%r{^#{REPO_PREFIX}}, "") + odebug "[user, repo, token] might be [#{user}, #{repo}, #{token}]" + [user, repo, token] + end +end diff --git a/Library/Homebrew/cask/lib/hbc/scopes.rb b/Library/Homebrew/cask/lib/hbc/scopes.rb new file mode 100644 index 0000000000..3fbb59d26d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/scopes.rb @@ -0,0 +1,59 @@ +module Hbc::Scopes + def self.included(base) + base.extend(ClassMethods) + end + + module ClassMethods + def all + @all_casks ||= {} + all_tokens.map { |t| @all_casks[t] ||= load(t) } + end + + def all_tapped_cask_dirs + @all_tapped_cask_dirs ||= Tap.names.map(&Tap.method(:fetch)).map(&:cask_dir) + .unshift(default_tap.cask_dir) # optimization: place the default Tap first + .uniq + end + + def reset_all_tapped_cask_dirs + # The memoized value should be reset when a Tap is added/removed + # (which is a rare event in our codebase). + @all_tapped_cask_dirs = nil + end + + def all_tokens + cask_tokens = all_tapped_cask_dirs.map { |d| Dir.glob d.join("*.rb") }.flatten + cask_tokens.map { |c| + # => "/usr/local/Library/Taps/caskroom/example-tap/Casks/example.rb" + c.sub!(%r{\.rb$}, "") + # => ".../example" + c = c.split("/").last 4 + # => ["caskroom", "example-tap", "Casks", "example"] + c.delete_at(-2) + # => ["caskroom", "example-tap", "example"] + c.join "/" + } + end + + def installed + # Hbc.load has some DWIM which is slow. Optimize here + # by spoon-feeding Hbc.load fully-qualified paths. + # TODO: speed up Hbc::Source::Tapped (main perf drag is calling Hbc.all_tokens repeatedly) + # TODO: ability to specify expected source when calling Hbc.load (minor perf benefit) + Pathname.glob(caskroom.join("*")) + .map { |caskroom_path| + token = caskroom_path.basename.to_s + + path_to_cask = all_tapped_cask_dirs.find { |tap_dir| + tap_dir.join("#{token}.rb").exist? + } + + if path_to_cask + Hbc.load(path_to_cask.join("#{token}.rb")) + else + Hbc.load(token) + end + } + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/source.rb b/Library/Homebrew/cask/lib/hbc/source.rb new file mode 100644 index 0000000000..af298108a7 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/source.rb @@ -0,0 +1,37 @@ +module Hbc::Source; end + +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::Source + def self.sources + [ + Hbc::Source::URI, + Hbc::Source::PathSlashRequired, + Hbc::Source::TappedQualified, + Hbc::Source::UntappedQualified, + Hbc::Source::Tapped, + Hbc::Source::PathSlashOptional, + Hbc::Source::Gone, + ] + end + + def self.for_query(query) + odebug "Translating '#{query}' into a valid Cask source" + raise Hbc::CaskUnavailableError, query if query.to_s =~ %r{^\s*$} + source = sources.find { |s| + odebug "Testing source class #{s}" + s.me?(query) + } + raise Hbc::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 diff --git a/Library/Homebrew/cask/lib/hbc/source/gone.rb b/Library/Homebrew/cask/lib/hbc/source/gone.rb new file mode 100644 index 0000000000..2b9f2b5f2f --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/source/gone.rb @@ -0,0 +1,19 @@ +class Hbc::Source::Gone + def self.me?(query) + Hbc::WithoutSource.new(query).installed? + end + + attr_reader :query + + def initialize(query) + @query = query + end + + def load + Hbc::WithoutSource.new(query) + end + + def to_s + "" + end +end diff --git a/Library/Homebrew/cask/lib/hbc/source/path_base.rb b/Library/Homebrew/cask/lib/hbc/source/path_base.rb new file mode 100644 index 0000000000..bbb413fd3c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/source/path_base.rb @@ -0,0 +1,65 @@ +require "rubygems" + +class Hbc::Source::PathBase + # derived classes must define method self.me? + + def self.path_for_query(query) + query_string = query.to_s + Pathname.new(query_string.end_with?(".rb") ? query_string : query_string + ".rb") + end + + attr_reader :path + + def initialize(path) + @path = Pathname(path).expand_path + end + + def load + raise Hbc::CaskError, "File '#{path}' does not exist" unless path.exist? + raise Hbc::CaskError, "File '#{path}' is not readable" unless path.readable? + raise Hbc::CaskError, "File '#{path}' is not a plain file" unless path.file? + load_cask + end + + def to_s + # stringify to fully-resolved location + path.to_s + end + + private + + def load_cask + instance_eval(cask_contents, __FILE__, __LINE__) + rescue Hbc::CaskError, StandardError, ScriptError => e + # bug: e.message.concat doesn't work with Hbc::CaskError exceptions + raise e, e.message.concat(" while loading '#{path}'") + end + + def cask_contents + File.open(path, "rb") do |handle| + contents = handle.read + if defined?(Encoding) + contents.force_encoding("UTF-8") + else + contents + end + end + end + + def cask(header_token, &block) + build_cask(Hbc::Cask, header_token, &block) + end + + def test_cask(header_token, &block) + build_cask(Hbc::TestCask, header_token, &block) + end + + def build_cask(cask_class, header_token, &block) + raise Hbc::CaskTokenDoesNotMatchError.new(cask_token, header_token) unless cask_token == header_token + cask_class.new(cask_token, sourcefile_path: path, &block) + end + + def cask_token + path.basename.to_s.sub(%r{\.rb}, "") + end +end diff --git a/Library/Homebrew/cask/lib/hbc/source/path_slash_optional.rb b/Library/Homebrew/cask/lib/hbc/source/path_slash_optional.rb new file mode 100644 index 0000000000..fb34c481a0 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/source/path_slash_optional.rb @@ -0,0 +1,8 @@ +require "hbc/source/path_base" + +class Hbc::Source::PathSlashOptional < Hbc::Source::PathBase + def self.me?(query) + path = path_for_query(query) + path.exist? + end +end diff --git a/Library/Homebrew/cask/lib/hbc/source/path_slash_required.rb b/Library/Homebrew/cask/lib/hbc/source/path_slash_required.rb new file mode 100644 index 0000000000..0c533a8a50 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/source/path_slash_required.rb @@ -0,0 +1,8 @@ +require "hbc/source/path_base" + +class Hbc::Source::PathSlashRequired < Hbc::Source::PathBase + def self.me?(query) + path = path_for_query(query) + path.to_s.include?("/") && path.exist? + end +end diff --git a/Library/Homebrew/cask/lib/hbc/source/tapped.rb b/Library/Homebrew/cask/lib/hbc/source/tapped.rb new file mode 100644 index 0000000000..da9366840c --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/source/tapped.rb @@ -0,0 +1,35 @@ +class Hbc::Source::Tapped + def self.me?(query) + path_for_query(query).exist? + end + + def self.path_for_query(query) + # Repeating Hbc.all_tokens is very slow for operations such as + # brew cask list, but memoizing the value might cause breakage + # elsewhere, given that installation and tap status is permitted + # to change during the course of an invocation. + token_with_tap = Hbc.all_tokens.find { |t| t.split("/").last == query.sub(%r{\.rb$}i, "") } + if token_with_tap + user, repo, token = token_with_tap.split("/") + Tap.fetch(user, repo).cask_dir.join("#{token}.rb") + else + Hbc.default_tap.cask_dir.join(query.sub(%r{(\.rb)?$}i, ".rb")) + end + end + + attr_reader :token + + def initialize(token) + @token = token + end + + def load + path = self.class.path_for_query(token) + Hbc::Source::PathSlashOptional.new(path).load + end + + def to_s + # stringify to fully-resolved location + self.class.path_for_query(token).expand_path.to_s + end +end diff --git a/Library/Homebrew/cask/lib/hbc/source/tapped_qualified.rb b/Library/Homebrew/cask/lib/hbc/source/tapped_qualified.rb new file mode 100644 index 0000000000..48f8501e54 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/source/tapped_qualified.rb @@ -0,0 +1,12 @@ +require "hbc/source/tapped" + +class Hbc::Source::TappedQualified < Hbc::Source::Tapped + def self.me?(query) + !Hbc::QualifiedToken.parse(query).nil? && path_for_query(query).exist? + end + + def self.path_for_query(query) + user, repo, token = Hbc::QualifiedToken.parse(query) + Tap.new(user, repo).cask_dir.join(token.sub(%r{(\.rb)?$}i, ".rb")) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/source/untapped_qualified.rb b/Library/Homebrew/cask/lib/hbc/source/untapped_qualified.rb new file mode 100644 index 0000000000..361919bb38 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/source/untapped_qualified.rb @@ -0,0 +1,11 @@ +require "hbc/source/tapped_qualified" + +class Hbc::Source::UntappedQualified < Hbc::Source::TappedQualified + def self.path_for_query(query) + user, repo, token = Hbc::QualifiedToken.parse(query) + + tap = Tap.fetch(user, repo) + tap.install unless tap.installed? + tap.cask_dir.join(token.sub(%r{(\.rb)?$}i, ".rb")) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/source/uri.rb b/Library/Homebrew/cask/lib/hbc/source/uri.rb new file mode 100644 index 0000000000..99bc506880 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/source/uri.rb @@ -0,0 +1,28 @@ +class Hbc::Source::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 Hbc::CaskUnavailableError, uri + end + Hbc::Source::PathSlashOptional.new(path).load + end + + def to_s + uri.to_s + end +end diff --git a/Library/Homebrew/cask/lib/hbc/staged.rb b/Library/Homebrew/cask/lib/hbc/staged.rb new file mode 100644 index 0000000000..7e2c935411 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/staged.rb @@ -0,0 +1,48 @@ +module Hbc::Staged + def info_plist_file(index = 0) + index = 0 if index == :first + index = 1 if index == :second + index = -1 if index == :last + Hbc.appdir.join(@cask.artifacts[:app].to_a.at(index).first, "Contents", "Info.plist") + end + + def plist_exec(cmd) + @command.run!("/usr/libexec/PlistBuddy", args: ["-c", cmd, info_plist_file]) + end + + def plist_set(key, value) + plist_exec("Set #{key} #{value}") + rescue StandardError => e + raise Hbc::CaskError, "#{@cask.token}: 'plist_set' failed with: #{e}" + end + + def bundle_identifier + plist_exec("Print CFBundleIdentifier").stdout.chomp + rescue StandardError => e + raise Hbc::CaskError, "#{@cask.token}: 'bundle_identifier' failed with: #{e}" + end + + def set_permissions(paths, permissions_str) + full_paths = remove_nonexistent(paths) + return if full_paths.empty? + @command.run!("/bin/chmod", args: ["-R", "--", permissions_str] + full_paths, + sudo: true) + end + + def set_ownership(paths, user: current_user, group: "staff") + full_paths = remove_nonexistent(paths) + return if full_paths.empty? + @command.run!("/usr/sbin/chown", args: ["-R", "--", "#{user}:#{group}"] + full_paths, + sudo: true) + end + + def current_user + Hbc::Utils.current_user + end + + private + + def remove_nonexistent(paths) + Array(paths).map { |p| Pathname(p).expand_path }.select(&:exist?) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/system_command.rb b/Library/Homebrew/cask/lib/hbc/system_command.rb new file mode 100644 index 0000000000..6fa8a901fa --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/system_command.rb @@ -0,0 +1,173 @@ +require "open3" +require "shellwords" + +class Hbc::SystemCommand + attr_reader :command + + def self.run(executable, options = {}) + new(executable, options).run! + end + + def self.run!(command, options = {}) + run(command, options.merge(must_succeed: true)) + end + + def run! + @processed_output = { stdout: "", stderr: "" } + odebug "Executing: #{expanded_command.utf8_inspect}" + + each_output_line do |type, line| + case type + when :stdout + processed_output[:stdout] << line + ohai line.chomp if options[:print_stdout] + when :stderr + processed_output[:stderr] << line + ohai line.chomp if options[:print_stderr] + end + end + + assert_success if options[:must_succeed] + result + end + + def initialize(executable, options) + @executable = executable + @options = options + process_options! + end + + private + + attr_reader :executable, :options, :processed_output, :processed_status + + def process_options! + options.assert_valid_keys :input, :print_stdout, :print_stderr, :args, :must_succeed, :sudo, :bsexec + sudo_prefix = %w[/usr/bin/sudo -E --] + bsexec_prefix = ["/bin/launchctl", "bsexec", options[:bsexec] == :startup ? "/" : options[:bsexec]] + @command = [executable] + options[:print_stderr] = true unless options.key?(:print_stderr) + @command.unshift(*bsexec_prefix) if options[:bsexec] + @command.unshift(*sudo_prefix) if options[:sudo] + @command.concat(options[:args]) if options.key?(:args) && !options[:args].empty? + @command[0] = Shellwords.shellescape(@command[0]) if @command.size == 1 + nil + end + + def assert_success + return if processed_status && processed_status.success? + raise Hbc::CaskCommandFailedError.new(command.utf8_inspect, processed_output[:stdout], processed_output[:stderr], processed_status) + end + + def expanded_command + @expanded_command ||= command.map { |arg| + if arg.respond_to?(:to_path) + File.absolute_path(arg) + else + String(arg) + end + } + end + + def each_output_line(&b) + raw_stdin, raw_stdout, raw_stderr, raw_wait_thr = + Open3.popen3(*expanded_command) + + write_input_to(raw_stdin) if options[:input] + raw_stdin.close_write + each_line_from [raw_stdout, raw_stderr], &b + + @processed_status = raw_wait_thr.value + end + + def write_input_to(raw_stdin) + Array(options[:input]).each { |line| raw_stdin.puts line } + end + + def each_line_from(sources) + loop do + readable_sources = IO.select(sources)[0] + readable_sources.delete_if(&:eof?).first(1).each do |source| + type = (source == sources[0] ? :stdout : :stderr) + begin + yield(type, source.readline_nonblock || "") + rescue IO::WaitReadable, EOFError + next + end + end + break if readable_sources.empty? + end + sources.each(&:close_read) + end + + def result + Hbc::SystemCommand::Result.new(command, + processed_output[:stdout], + processed_output[:stderr], + processed_status.exitstatus) + end +end + +class Hbc::SystemCommand::Result + attr_accessor :command, :stdout, :stderr, :exit_status + + def initialize(command, stdout, stderr, exit_status) + @command = command + @stdout = stdout + @stderr = stderr + @exit_status = exit_status + end + + def plist + @plist ||= self.class._parse_plist(@command, @stdout.dup) + end + + def success? + @exit_status == 0 + end + + def merged_output + @merged_output ||= @stdout + @stderr + end + + def to_s + @stdout + end + + def self._warn_plist_garbage(command, garbage) + return true unless garbage =~ %r{\S} + external = File.basename(command.first) + lines = garbage.strip.split("\n") + opoo "Non-XML stdout from #{external}:" + $stderr.puts lines.map { |l| " #{l}" } + end + + def self._parse_plist(command, output) + raise Hbc::CaskError, "Empty plist input" unless output =~ %r{\S} + output.sub!(%r{\A(.*?)(<\?\s*xml)}m, '\2') + _warn_plist_garbage(command, Regexp.last_match[1]) if Hbc.debug + output.sub!(%r{(<\s*/\s*plist\s*>)(.*?)\Z}m, '\1') + _warn_plist_garbage(command, Regexp.last_match[2]) + xml = Plist.parse_xml(output) + unless xml.respond_to?(:keys) && !xml.keys.empty? + raise Hbc::CaskError, <<-ERRMSG +Empty result parsing plist output from command. + command was: + #{command.utf8_inspect} + output we attempted to parse: + #{output} + ERRMSG + end + xml + rescue Plist::ParseError => e + raise Hbc::CaskError, <<-ERRMSG +Error parsing plist output from command. + command was: + #{command.utf8_inspect} + error was: + #{e} + output we attempted to parse: + #{output} + ERRMSG + end +end diff --git a/Library/Homebrew/cask/lib/hbc/topological_hash.rb b/Library/Homebrew/cask/lib/hbc/topological_hash.rb new file mode 100644 index 0000000000..bbad1bb4d9 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/topological_hash.rb @@ -0,0 +1,12 @@ +require "tsort" + +# a basic topologically sortable hashmap +class Hbc::TopologicalHash < Hash + include TSort + + alias tsort_each_node each_key + + def tsort_each_child(node, &block) + fetch(node).each(&block) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/underscore_supporting_uri.rb b/Library/Homebrew/cask/lib/hbc/underscore_supporting_uri.rb new file mode 100644 index 0000000000..34bfea387b --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/underscore_supporting_uri.rb @@ -0,0 +1,26 @@ +require "uri" + +module Hbc::UnderscoreSupportingURI + def self.parse(maybe_uri) + return nil if maybe_uri.nil? + URI.parse(maybe_uri) + rescue URI::InvalidURIError => e + scheme, host, path = simple_parse(maybe_uri) + raise e unless path && host.include?("_") + URI.parse(without_host_underscores(scheme, host, path)).tap do |uri| + uri.instance_variable_set("@host", host) + end + end + + def self.simple_parse(maybe_uri) + scheme, host_and_path = maybe_uri.split("://") + host, path = host_and_path.split("/", 2) + [scheme, host, path] + rescue StandardError + nil + end + + def self.without_host_underscores(scheme, host, path) + ["#{scheme}:/", host.tr("_", "-"), path].join("/") + end +end diff --git a/Library/Homebrew/cask/lib/hbc/url.rb b/Library/Homebrew/cask/lib/hbc/url.rb new file mode 100644 index 0000000000..5f763ca8a0 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/url.rb @@ -0,0 +1,37 @@ +require "forwardable" + +class Hbc::URL + FAKE_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10) http://caskroom.io".freeze + + attr_reader :using, :revision, :trust_cert, :uri, :cookies, :referer, :data + + extend Forwardable + def_delegators :uri, :path, :scheme, :to_s + + def self.from(*args, &block) + if block_given? + Hbc::DSL::StanzaProxy.once(self) { new(*block.call) } + else + new(*args) + end + end + + def initialize(uri, options = {}) + @uri = Hbc::UnderscoreSupportingURI.parse(uri) + @user_agent = options[:user_agent] + @cookies = options[:cookies] + @referer = options[:referer] + @using = options[:using] + @revision = options[:revision] + @trust_cert = options[:trust_cert] + @data = options[:data] + end + + def user_agent + if @user_agent == :fake + FAKE_USER_AGENT + else + @user_agent + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/url_checker.rb b/Library/Homebrew/cask/lib/hbc/url_checker.rb new file mode 100644 index 0000000000..8737903df1 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/url_checker.rb @@ -0,0 +1,75 @@ +require "hbc/checkable" + +class Hbc::UrlChecker + attr_accessor :cask, :response_status, :headers + + include Hbc::Checkable + + def initialize(cask, fetcher = Hbc::Fetcher) + @cask = cask + @fetcher = fetcher + @headers = {} + end + + def summary_header + "url check result for #{cask}" + end + + def run + _get_data_from_request + return if errors? + _check_response_status + end + + HTTP_RESPONSES = [ + "HTTP/1.0 200 OK", + "HTTP/1.1 200 OK", + "HTTP/1.1 302 Found", + ].freeze + + OK_RESPONSES = { + "http" => HTTP_RESPONSES, + "https" => HTTP_RESPONSES, + "ftp" => ["OK"], + }.freeze + + def _check_response_status + ok = OK_RESPONSES[cask.url.scheme] + return if ok.include?(@response_status) + add_error "unexpected http response, expecting #{ok.map(&:utf8_inspect).join(' or ')}, got #{@response_status.utf8_inspect}" + end + + def _get_data_from_request + response = @fetcher.head(cask.url) + + if response.empty? + add_error "timeout while requesting #{cask.url}" + return + end + + response_lines = response.split("\n").map(&:chomp) + + case cask.url.scheme + when "http", "https" then + @response_status = response_lines.grep(%r{^HTTP}).last + if @response_status.respond_to?(:strip) + @response_status.strip! + unless response_lines.index(@response_status).nil? + http_headers = response_lines[(response_lines.index(@response_status) + 1)..-1] + http_headers.each do |line| + header_name, header_value = line.split(": ") + @headers[header_name] = header_value + end + end + end + when "ftp" then + @response_status = "OK" + response_lines.each do |line| + header_name, header_value = line.split(": ") + @headers[header_name] = header_value + end + else + add_error "unknown scheme for #{cask.url}" + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/utils.rb b/Library/Homebrew/cask/lib/hbc/utils.rb new file mode 100644 index 0000000000..6fc52cc939 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/utils.rb @@ -0,0 +1,198 @@ +module Hbc::Utils; end + +require "yaml" +require "open3" +require "stringio" + +require "hbc/utils/file" +require "hbc/utils/tty" + +UPDATE_CMD = "brew uninstall --force brew-cask; brew untap phinze/cask; brew untap caskroom/cask; brew update; brew cleanup; brew cask cleanup".freeze +ISSUES_URL = "https://github.com/caskroom/homebrew-cask#reporting-bugs".freeze + +# monkeypatch Object - not a great idea +class Object + def utf8_inspect + return inspect unless defined?(Encoding) + return map(&:utf8_inspect) if respond_to?(:map) + inspect.force_encoding("UTF-8").sub(%r{\A"(.*)"\Z}, '\1') + end +end + +class Buffer < StringIO + def initialize(tty = false) + super() + @tty = tty + end + + def tty? + @tty + end +end + +# global methods + +def odebug(title, *sput) + if Hbc.respond_to?(:debug) && Hbc.debug + width = Hbc::Utils::Tty.width * 4 - 6 + if $stdout.tty? && title.to_s.length > width + title = title.to_s[0, width - 3] + "..." + end + puts "#{Hbc::Utils::Tty.magenta.bold}==>#{Hbc::Utils::Tty.reset.bold} #{title}#{Hbc::Utils::Tty.reset}" + puts sput unless sput.empty? + end +end + +module Hbc::Utils + def self.which(cmd, path = ENV["PATH"]) + unless File.basename(cmd) == cmd.to_s + # cmd contains a directory element + cmd_pn = Pathname(cmd) + return nil unless cmd_pn.absolute? + return resolve_executable(cmd_pn) + end + path.split(File::PATH_SEPARATOR).each do |elt| + fq_cmd = Pathname(elt).expand_path.join(cmd) + resolved = resolve_executable fq_cmd + return resolved if resolved + end + nil + end + + def self.resolve_executable(cmd) + cmd_pn = Pathname(cmd) + return nil unless cmd_pn.exist? + return nil unless cmd_pn.executable? + begin + cmd_pn = Pathname(cmd_pn.realpath) + rescue RuntimeError + return nil + end + return nil unless cmd_pn.file? + cmd_pn + end + + def self.gain_permissions_remove(path, command: Hbc::SystemCommand) + if path.respond_to?(:rmtree) && path.exist? + gain_permissions(path, ["-R"], command, &:rmtree) + elsif File.symlink?(path) + gain_permissions(path, ["-h"], command, &FileUtils.method(:rm_f)) + end + end + + def self.gain_permissions(path, command_args, command) + tried_permissions = false + tried_ownership = false + begin + yield path + rescue StandardError + # in case of permissions problems + unless tried_permissions + # TODO: Better handling for the case where path is a symlink. + # The -h and -R flags cannot be combined, and behavior is + # dependent on whether the file argument has a trailing + # slash. This should do the right thing, but is fragile. + command.run("/usr/bin/chflags", + must_succeed: false, + args: command_args + ["--", "000", path]) + command.run("/bin/chmod", + must_succeed: false, + args: command_args + ["--", "u+rwx", path]) + command.run("/bin/chmod", + must_succeed: false, + args: command_args + ["-N", path]) + tried_permissions = true + retry # rmtree + end + unless tried_ownership + # in case of ownership problems + # TODO: Further examine files to see if ownership is the problem + # before using sudo+chown + ohai "Using sudo to gain ownership of path '#{path}'" + command.run("/usr/sbin/chown", + args: command_args + ["--", current_user, path], + sudo: true) + tried_ownership = true + # retry chflags/chmod after chown + tried_permissions = false + retry # rmtree + end + end + end + + def self.current_user + Etc.getpwuid(Process.euid).name + end + + # paths that "look" descendant (textually) will still + # return false unless both the given paths exist + def self.file_is_descendant(file, dir) + file = Pathname.new(file) + dir = Pathname.new(dir) + return false unless file.exist? && dir.exist? + unless dir.directory? + onoe "Argument must be a directory: '#{dir}'" + return false + end + unless file.absolute? && dir.absolute? + onoe "Both arguments must be absolute: '#{file}', '#{dir}'" + return false + end + while file.parent != file + return true if File.identical?(file, dir) + file = file.parent + end + false + end + + def self.path_occupied?(path) + File.exist?(path) || File.symlink?(path) + end + + def self.error_message_with_suggestions + <<-EOS.undent + #{Hbc::Utils::Tty.reset.bold} + Most likely, this means you have an outdated version of Homebrew-Cask. Please run: + + #{Hbc::Utils::Tty.green.normal}#{UPDATE_CMD} + + #{Hbc::Utils::Tty.reset.bold}If this doesn’t fix the problem, please report this bug: + + #{Hbc::Utils::Tty.underline}#{ISSUES_URL}#{Hbc::Utils::Tty.reset} + + EOS + end + + def self.method_missing_message(method, token, section = nil) + poo = [] + poo << "Unexpected method '#{method}' called" + poo << "during #{section}" if section + poo << "on Cask #{token}." + + opoo(poo.join(" ") + "\n" + error_message_with_suggestions) + end + + def self.nowstamp_metadata_path(container_path) + @timenow ||= Time.now.gmtime + if container_path.respond_to?(:join) + precision = 3 + timestamp = @timenow.strftime("%Y%m%d%H%M%S") + fraction = format("%.#{precision}f", @timenow.to_f - @timenow.to_i)[1..-1] + timestamp.concat(fraction) + container_path.join(timestamp) + end + end + + def self.size_in_bytes(files) + Array(files).reduce(0) { |a, e| a + (File.size?(e) || 0) } + end + + def self.capture_stderr + previous_stderr = $stderr + $stderr = StringIO.new + yield + $stderr.string + ensure + $stderr = previous_stderr + end +end diff --git a/Library/Homebrew/cask/lib/hbc/utils/file.rb b/Library/Homebrew/cask/lib/hbc/utils/file.rb new file mode 100644 index 0000000000..967c6834f2 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/utils/file.rb @@ -0,0 +1,12 @@ +module Hbc::Utils + module_function + + def file_locked?(file) + unlocked = File.open(file).flock(File::LOCK_EX | File::LOCK_NB) + # revert lock if file was unlocked before check + File.open(file).flock(File::LOCK_UN) if unlocked + !unlocked + rescue + true + end +end diff --git a/Library/Homebrew/cask/lib/hbc/utils/tty.rb b/Library/Homebrew/cask/lib/hbc/utils/tty.rb new file mode 100644 index 0000000000..c383df828b --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/utils/tty.rb @@ -0,0 +1,125 @@ +# originally from Homebrew utils.rb + +class Hbc::Utils::Tty + COLORS = { + black: 0, + red: 1, + green: 2, + yellow: 3, + blue: 4, + magenta: 5, + cyan: 6, + white: 7, + default: 9, + }.freeze + + ATTRIBUTES = { + reset: 0, + bold: 1, + dim: 2, + italic: 3, + underline: 4, + blink: 5, + inverse: 7, + invisible: 8, + strikethrough: 9, + normal: 22, + }.freeze + + @sequence = [] + + class << self + COLORS.keys.each do |sym| + define_method(sym) do + foreground(COLORS[sym]) + end + define_method("fg_#{sym}".to_sym) do + foreground(COLORS[sym]) + end + define_method("bg_#{sym}".to_sym) do + background(COLORS[sym]) + end + end + + ATTRIBUTES.keys.each do |sym| + define_method(sym) do + deferred_emit(ATTRIBUTES[sym]) + end + end + + def width + `/usr/bin/tput cols`.strip.to_i + end + + def truncate(str) + str.to_s[0, width - 4] + end + + private + + def foreground(color) + deferred_emit(to_foreground_code(color)) + end + + def background(color) + deferred_emit(to_background_code(color)) + end + + def to_color_code(space, color) + return unless (num = to_color_number(color)) + return space + num if num < space + return space + 9 if num > space + num + end + + def to_foreground_code(color) + to_color_code(30, color) + end + + def to_background_code(color) + to_color_code(40, color) + end + + def to_color_number(color) + COLORS[color] || color.is_a?(Integer) ? color : nil + end + + def to_attribute_number(attribute) + ATTRIBUTES[attribute] || attribute.is_a?(Integer) ? attribute : nil + end + + def sanitize_integer(arg) + return arg.to_i if arg.is_a?(Integer) + return 0 if arg.to_s =~ %r{^0+$} + if arg.respond_to?(:to_i) && (int = arg.to_i) > 0 + return int + end + $stderr.puts "Warning: bad Tty code #{arg}" + ATTRIBUTES[:reset] + end + + def deferred_emit(*codes) + @sequence.concat Array(*codes).map(&method(:sanitize_integer)) + Hbc::Utils::Tty + end + + def to_s + sequence = @sequence + @sequence = [] + return "" unless $stdout.tty? + if sequence.empty? + $stderr.puts "Warning: empty Tty sequence" + sequence = [ATTRIBUTES[:reset]] + end + "#{initiate}#{sequence.join(';')}#{terminate}" + end + + def initiate + "\033[" + end + + def terminate + "m" + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/verify.rb b/Library/Homebrew/cask/lib/hbc/verify.rb new file mode 100644 index 0000000000..d3c2713e7d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/verify.rb @@ -0,0 +1,33 @@ +module Hbc::Verify; end + +require "hbc/verify/checksum" +require "hbc/verify/gpg" + +module Hbc::Verify + module_function + + def verifications + [ + Hbc::Verify::Checksum + # TODO: Hbc::Verify::Gpg + ] + end + + def all(cask, downloaded_path) + odebug "Verifying download" + verifications = for_cask(cask) + odebug "#{verifications.size} verifications defined", verifications + verifications.each do |verification| + odebug "Running verification of class #{verification}" + verification.new(cask, downloaded_path).verify + end + end + + def for_cask(cask) + odebug "Determining which verifications to run for Cask #{cask}" + verifications.select do |verification| + odebug "Checking for verification class #{verification}" + verification.me?(cask) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/verify/checksum.rb b/Library/Homebrew/cask/lib/hbc/verify/checksum.rb new file mode 100644 index 0000000000..3af6f1667f --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/verify/checksum.rb @@ -0,0 +1,43 @@ +require "digest" + +class Hbc::Verify::Checksum + def self.me?(cask) + return true unless cask.sha256 == :no_check + ohai "No checksum defined for Cask #{cask}, skipping verification" + false + end + + attr_reader :cask, :downloaded_path + + def initialize(cask, downloaded_path) + @cask = cask + @downloaded_path = downloaded_path + end + + def verify + return unless self.class.me?(cask) + ohai "Verifying checksum for Cask #{cask}" + verify_checksum + end + + private + + def expected + @expected ||= cask.sha256 + end + + def computed + @computed ||= Digest::SHA2.file(downloaded_path).hexdigest + end + + def verify_checksum + raise Hbc::CaskSha256MissingError, "sha256 required: sha256 '#{computed}'" if expected.nil? || expected.empty? + + if expected == computed + odebug "SHA256 checksums match" + else + ohai 'Note: running "brew update" may fix sha256 checksum errors' + raise Hbc::CaskSha256MismatchError.new(downloaded_path, expected, computed) + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/verify/gpg.rb b/Library/Homebrew/cask/lib/hbc/verify/gpg.rb new file mode 100644 index 0000000000..6190f67d14 --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/verify/gpg.rb @@ -0,0 +1,60 @@ +class Hbc::Verify::Gpg + def self.me?(cask) + cask.gpg + end + + attr_reader :cask, :downloaded_path + + def initialize(cask, downloaded_path, command = Hbc::SystemCommand) + @command = command + @cask = cask + @downloaded_path = downloaded_path + end + + def available? + return @available unless @available.nil? + @available = self.class.me?(cask) && installed? + end + + def installed? + cmd = @command.run("/usr/bin/type", + args: ["-p", "gpg"]) + + # if `gpg` is found, return its absolute path + cmd.success? ? cmd.stdout : false + end + + def fetch_sig(force = false) + unversioned_cask = cask.version.is_a?(Symbol) + cached = cask.metadata_subdir("gpg") unless unversioned_cask + + meta_dir = cached || cask.metadata_subdir("gpg", :now, true) + sig_path = meta_dir.join("signature.asc") + + curl(cask.gpg.signature, "-o", sig_path.to_s) unless cached || force + + sig_path + end + + def import_key + args = if cask.gpg.key_id + ["--recv-keys", cask.gpg.key_id] + elsif cask.gpg.key_url + ["--fetch-key", cask.gpg.key_url.to_s] + end + + @command.run!("gpg", args: args) + end + + def verify + return unless available? + import_key + sig = fetch_sig + + ohai "Verifying GPG signature for #{cask}" + + @command.run!("gpg", + args: ["--verify", sig, downloaded_path], + print_stdout: true) + end +end diff --git a/Library/Homebrew/cask/lib/hbc/version.rb b/Library/Homebrew/cask/lib/hbc/version.rb new file mode 100644 index 0000000000..471fd1999d --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/version.rb @@ -0,0 +1,13 @@ +HBC_VERSION = "0.60.0".freeze + +module Hbc + def self.full_version + @full_version ||= begin + revision, commit = Dir.chdir(Hbc.default_tap.path) do + [`git rev-parse --short=4 --verify -q HEAD 2>/dev/null`.chomp, + `git show -s --format="%cr" HEAD 2>/dev/null`.chomp] + end + "#{HBC_VERSION} (git revision #{revision}; last commit #{commit})" + end + end +end diff --git a/Library/Homebrew/cask/lib/hbc/without_source.rb b/Library/Homebrew/cask/lib/hbc/without_source.rb new file mode 100644 index 0000000000..6ed826e41e --- /dev/null +++ b/Library/Homebrew/cask/lib/hbc/without_source.rb @@ -0,0 +1,15 @@ +class Hbc::WithoutSource < Hbc::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 diff --git a/Library/Homebrew/cask/lib/vendor/plist.rb b/Library/Homebrew/cask/lib/vendor/plist.rb new file mode 100644 index 0000000000..9e469a0693 --- /dev/null +++ b/Library/Homebrew/cask/lib/vendor/plist.rb @@ -0,0 +1,234 @@ +# +# = plist +# +# Copyright 2006-2010 Ben Bleything and Patrick May +# Distributed under the MIT License +# + +# Plist parses macOS xml property list files into ruby data structures. +# +# === Load a plist file +# This is the main point of the library: +# +# r = Plist::parse_xml( filename_or_xml ) +module Plist +# Note that I don't use these two elements much: +# +# + Date elements are returned as DateTime objects. +# + Data elements are implemented as Tempfiles +# +# Plist::parse_xml will blow up if it encounters a Date element. +# If you encounter such an error, or if you have a Date element which +# can't be parsed into a Time object, please send your plist file to +# plist@hexane.org so that I can implement the proper support. + def Plist::parse_xml( filename_or_xml ) + listener = Listener.new + #parser = REXML::Parsers::StreamParser.new(File.new(filename), listener) + parser = StreamParser.new(filename_or_xml, listener) + parser.parse + listener.result + end + + class Listener + #include REXML::StreamListener + + attr_accessor :result, :open + + def initialize + @result = nil + @open = Array.new + end + + + def tag_start(name, attributes) + @open.push PTag::mappings[name].new + end + + def text( contents ) + @open.last.text = contents if @open.last + end + + def tag_end(name) + last = @open.pop + if @open.empty? + @result = last.to_ruby + else + @open.last.children.push last + end + end + end + + class StreamParser + def initialize( plist_data_or_file, listener ) + if plist_data_or_file.respond_to? :read + @xml = plist_data_or_file.read + elsif File.exists? plist_data_or_file + @xml = File.read( plist_data_or_file ) + else + @xml = plist_data_or_file + end + + trim_to_xml_start! + + @listener = listener + end + + def trim_to_xml_start! + _, xml_tag, rest = @xml.partition(/^<\?xml/) + @xml = [xml_tag, rest].join + end + + TEXT = /([^<]+)/ + XMLDECL_PATTERN = /<\?xml\s+(.*?)\?>*/um + DOCTYPE_PATTERN = /\s*)/um + COMMENT_START = /\A/um + + + def parse + plist_tags = PTag::mappings.keys.join('|') + start_tag = /<(#{plist_tags})([^>]*)>/i + end_tag = /<\/(#{plist_tags})[^>]*>/i + + require 'strscan' + + @scanner = StringScanner.new( @xml ) + until @scanner.eos? + if @scanner.scan(COMMENT_START) + @scanner.scan(COMMENT_END) + elsif @scanner.scan(XMLDECL_PATTERN) + elsif @scanner.scan(DOCTYPE_PATTERN) + elsif @scanner.scan(start_tag) + @listener.tag_start(@scanner[1], nil) + if (@scanner[2] =~ /\/$/) + @listener.tag_end(@scanner[1]) + end + elsif @scanner.scan(TEXT) + @listener.text(@scanner[1]) + elsif @scanner.scan(end_tag) + @listener.tag_end(@scanner[1]) + else + raise ParseError.new("Unimplemented element #{@xml}") + end + end + end + end + + class PTag + @@mappings = { } + def PTag::mappings + @@mappings + end + + def PTag::inherited( sub_class ) + key = sub_class.to_s.downcase + key.gsub!(/^plist::/, '' ) + key.gsub!(/^p/, '') unless key == "plist" + + @@mappings[key] = sub_class + end + + attr_accessor :text, :children + def initialize + @children = Array.new + end + + def to_ruby + raise ParseError.new("Unimplemented: " + self.class.to_s + "#to_ruby on #{self.inspect}") + end + end + + class PList < PTag + def to_ruby + children.first.to_ruby if children.first + end + end + + class PDict < PTag + def to_ruby + dict = Hash.new + key = nil + + children.each do |c| + if key.nil? + key = c.to_ruby + else + dict[key] = c.to_ruby + key = nil + end + end + + dict + end + end + + require 'cgi' + class PKey < PTag + def to_ruby + CGI::unescapeHTML(text || '') + end + end + + class PString < PTag + def to_ruby + CGI::unescapeHTML(text || '') + end + end + + class PArray < PTag + def to_ruby + children.collect do |c| + c.to_ruby + end + end + end + + class PInteger < PTag + def to_ruby + text.to_i + end + end + + class PTrue < PTag + def to_ruby + true + end + end + + class PFalse < PTag + def to_ruby + false + end + end + + class PReal < PTag + def to_ruby + text.to_f + end + end + + require 'date' + class PDate < PTag + def to_ruby + DateTime.parse(text) + end + end + + require 'base64' + class PData < PTag + def to_ruby + data = Base64.decode64(text.gsub(/\s+/, '')) + + begin + return Marshal.load(data) + rescue Exception => e + io = StringIO.new + io.write data + io.rewind + return io + end + end + end + + class ParseError < RuntimeError; end +end diff --git a/Library/Homebrew/cask/man/man1/brew-cask.1 b/Library/Homebrew/cask/man/man1/brew-cask.1 new file mode 100644 index 0000000000..348a34b419 --- /dev/null +++ b/Library/Homebrew/cask/man/man1/brew-cask.1 @@ -0,0 +1,291 @@ +.\" generated with Ronn/v0.7.3 +.\" http://github.com/rtomayko/ronn/tree/0.7.3 +. +.TH "BREW\-CASK" "1" "August 2016" "Homebrew-Cask" "brew-cask" +. +.SH "NAME" +\fBbrew\-cask\fR \- a friendly binary installer for macOS +. +.SH "SYNOPSIS" +\fBbrew cask\fR command [options] [ \fItoken\fR \.\.\. ] +. +.SH "DESCRIPTION" +Homebrew\-Cask is a tool for installing precompiled macOS binaries (such as Applications) from the command line\. The user is never required to use the graphical user interface\. +. +.SH "ALPHA\-QUALITY SOFTWARE" +Homebrew\-Cask works robustly enough that we welcome new users, but the project is still in early development\. That means command names, option names, and other aspects of this manual are still subject to change\. +. +.SH "FREQUENTLY USED COMMANDS" +. +.TP +\fBinstall [\-\-force] [\-\-skip\-cask\-deps] [\-\-require\-sha]\fR \fItoken\fR [ \fItoken\fR \.\.\. ] +Install Cask identified by \fItoken\fR\. +. +.TP +\fBuninstall [\-\-force]\fR \fItoken\fR [ \fItoken\fR \.\.\. ] +Uninstall Cask identified by \fItoken\fR\. +. +.TP +\fBsearch\fR \fItext\fR | /\fIregexp\fR/ +Perform a substring search of known Cask tokens for \fItext\fR\. If the text is delimited by slashes, it is interpreted as a Ruby regular expression\. +. +.IP +The tokens returned by \fBsearch\fR are suitable as arguments for most other commands, such as \fBinstall\fR or \fBuninstall\fR\. +. +.SH "COMMANDS" +. +.TP +\fBaudit\fR [ \fItoken\fR \.\.\. ] +Check the given Casks for installability\. If no tokens are given on the command line, all Casks are audited\. +. +.TP +\fBcat\fR \fItoken\fR [ \fItoken\fR \.\.\. ] +Dump the given Cask definition file to the standard output\. +. +.TP +\fBcleanup\fR [\-\-outdated] +Clean up cached downloads and tracker symlinks\. With \fB\-\-outdated\fR, only clean up cached downloads older than 10 days old\. +. +.TP +\fBcreate\fR \fItoken\fR +Generate a Cask definition file for the Cask identified by \fItoken\fR and open a template for it in your favorite editor\. +. +.TP +\fBdoctor\fR or \fBdr\fR +Check for configuration issues\. Can be useful to upload as a gist for developers along with a bug report\. +. +.TP +\fBedit\fR \fItoken\fR +Open the given Cask definition file for editing\. +. +.TP +\fBfetch\fR [\-\-force] \fItoken\fR [ \fItoken\fR \.\.\. ] +Download remote application files for the given Cask to the local cache\. With \fB\-\-force\fR, force re\-download even if the files are already cached\. +. +.TP +\fBhome\fR or \fBhomepage\fR [ \fItoken\fR \.\.\. ] +Display the homepage associated with a given Cask in a browser\. +. +.IP +With no arguments, display the project page \fIhttp://caskroom\.io\fR\. +. +.TP +\fBinfo\fR or \fBabv\fR \fItoken\fR [ \fItoken\fR \.\.\. ] +Display information about the given Cask\. +. +.TP +\fBinstall [\-\-force] [\-\-skip\-cask\-deps] [\-\-require\-sha]\fR \fItoken\fR [ \fItoken\fR \.\.\. ] +Install the given Cask\. With \fB\-\-force\fR, re\-install even if the Cask appears to be already present\. With \fB\-\-skip\-cask\-deps\fR, skip any Cask dependencies\. \fB\-\-require\-sha\fR will abort installation if the Cask does not have a checksum defined\. +. +.IP +\fItoken\fR is usually the ID of a Cask as returned by \fBbrew cask search\fR, but see \fIOTHER WAYS TO SPECIFY A CASK\fR for variations\. +. +.TP +\fBlist\fR or \fBls\fR [\-1 | \-l] [ \fItoken\fR \.\.\. ] +Without any arguments, list all installed Casks\. With \fB\-1\fR, always format the output in a single column\. With \fB\-l\fR, give a more detailed listing\. +. +.IP +If \fItoken\fR is given, summarize the staged files associated with the given Cask\. +. +.TP +\fBsearch\fR or \fB\-S\fR +Display all Casks available for install\. +. +.TP +\fBsearch\fR or \fB\-S\fR \fItext\fR | /\fIregexp\fR/ +Perform a substring search of known Cask tokens for \fItext\fR\. If the text is delimited by slashes, it is interpreted as a Ruby regular expression\. +. +.TP +\fBstyle\fR [\-\-fix] [ \fItoken\fR \.\.\. ] +Check the given Casks for correct style using RuboCop Cask \fIhttps://github\.com/caskroom/rubocop\-cask\fR\. If no tokens are given on the command line, all Casks are checked\. With \fB\-\-fix\fR, auto\-correct any style errors if possible\. +. +.TP +\fBuninstall [\-\-force]\fR or \fBrm\fR or \fBremove\fR \fItoken\fR [ \fItoken\fR \.\.\. ] +Uninstall the given Cask\. With \fB\-\-force\fR, uninstall even if the Cask does not appear to be present\. +. +.IP +Note that \fBuninstall \-\-force\fR is currently imperfect\. It will follow the \fBuninstall\fR instructions from \fInewest\fR Cask definition, even if the given Cask has changed since you installed it\. The result is that \fBuninstall \-\-force\fR will always succeed in removing relevant files under \fB\fR, but will sometimes fail to remove relevant installed files outside of it\. This issue is being addressed\. +. +.IP +\fBuninstall\fR without \fB\-\-force\fR is also imperfect\. It may be unable to perform an \fBuninstall\fR operation if the given Cask has changed since you installed it\. This issue is being addressed\. +. +.TP +\fBupdate\fR +For convenience\. \fBbrew cask update\fR is a synonym for \fBbrew update\fR\. +. +.TP +\fBzap\fR \fItoken\fR [ \fItoken\fR \.\.\. ] +Unconditionally remove \fIall\fR files associated with the given Cask\. +. +.IP +Implicitly performs all actions associated with \fBuninstall\fR, even if the Cask does not appear to be currently installed\. +. +.IP +Removes all staged versions of the Cask distribution found under \fB/\fR\. +. +.IP +If the Cask definition contains a \fBzap\fR stanza, performs additional \fBzap\fR actions as defined there, such as removing local preference files\. \fBzap\fR actions are variable, depending on the level of detail defined by the Cask author\. +. +.IP +\fB\fBzap\fR may remove files which are shared between applications\.\fR +. +.SH "OPTIONS" +To make these options persistent, see the ENVIRONMENT section, below\. +. +.P +Some of these (such as \fB\-\-prefpanedir\fR) may be subject to removal in a future version\. +. +.TP +\fB\-\-force\fR +Force an install to proceed even when a previously\-existing install is detected\. +. +.TP +\fB\-\-skip\-cask\-deps\fR +Skip Cask dependencies when installing\. +. +.TP +\fB\-\-require\-sha\fR +Abort Cask installation if the Cask does not have a checksum defined\. +. +.TP +\fB\-\-caskroom=\fR +Location of the Caskroom, where all binaries are stored\. The default value is: \fB$(brew \-\-repository)/Caskroom\fR\. +. +.TP +\fB\-\-verbose\fR +Give additional feedback during installation\. +. +.TP +\fB\-\-appdir=\fR +Target location for Applications\. The default value is \fB/Applications\fR\. +. +.TP +\fB\-\-colorpickerdir=\fR +Target location for Color Pickers\. The default value is \fB~/Library/ColorPickers\fR\. +. +.TP +\fB\-\-prefpanedir=\fR +Target location for Preference Panes\. The default value is \fB~/Library/PreferencePanes\fR\. +. +.TP +\fB\-\-qlplugindir=\fR +Target location for QuickLook Plugins\. The default value is \fB~/Library/QuickLook\fR\. +. +.TP +\fB\-\-fontdir=\fR +Target location for Fonts\. The default value is \fB~/Library/Fonts\fR\. +. +.TP +\fB\-\-servicedir=\fR +Target location for Services\. The default value is \fB~/Library/Services\fR\. +. +.TP +\fB\-\-input_methoddir=\fR +Target location for Input Methods\. The default value is \fB~/Library/Input Methods\fR\. +. +.TP +\fB\-\-internet_plugindir=\fR +Target location for Internet Plugins\. The default value is \fB~/Library/Internet Plug\-Ins\fR\. +. +.TP +\fB\-\-audio_unit_plugindir=\fR +Target location for Audio Unit Plugins\. The default value is \fB~/Library/Audio/Plug\-Ins/Components\fR\. +. +.TP +\fB\-\-vst_plugindir=\fR +Target location for VST Plugins\. The default value is \fB~/Library/Audio/Plug\-Ins/VST\fR\. +. +.TP +\fB\-\-vst3_plugindir=\fR +Target location for VST3 Plugins\. The default value is \fB~/Library/Audio/Plug\-Ins/VST3\fR\. +. +.TP +\fB\-\-screen_saverdir=\fR +Target location for Screen Savers\. The default value is \fB~/Library/Screen Savers\fR\. +. +.TP +\fB\-\-no\-binaries\fR +Do not link "helper" executables to \fB/usr/local/bin\fR\. +. +.TP +\fB\-\-debug\fR +Output debugging information of use to Cask authors and developers\. +. +.SH "INTERACTION WITH HOMEBREW" +Homebrew\-Cask is implemented as a external command for Homebrew\. That means this project is entirely built upon the Homebrew infrastructure\. For example, upgrades to the Homebrew\-Cask tool are received through Homebrew: +. +.IP "" 4 +. +.nf + +brew update; brew cleanup; brew cask cleanup +. +.fi +. +.IP "" 0 +. +.P +And updates to individual Cask definitions are received whenever you issue the Homebrew command: +. +.IP "" 4 +. +.nf + +brew update +. +.fi +. +.IP "" 0 +. +.SH "OTHER WAYS TO SPECIFY A CASK" +Most Homebrew\-Cask commands can accept a Cask token as an argument\. As described above, the argument can take the form of: +. +.IP "\(bu" 4 +A token as returned by \fBbrew cask search\fR, \fIeg\fR \fBgoogle\-chrome\fR +. +.IP "" 0 +. +.P +Homebrew\-Cask also accepts three other forms in place of plain tokens: +. +.IP "\(bu" 4 +A fully\-qualified token which includes the Tap name, \fIeg\fR \fBcaskroom/fonts/font\-symbola\fR +. +.IP "\(bu" 4 +A fully\-qualified pathname to a Cask file, \fIeg\fR \fB/usr/local/Library/Taps/caskroom/homebrew\-cask/Casks/google\-chrome\.rb\fR +. +.IP "\(bu" 4 +A \fBcurl\fR\-retrievable URI to a Cask file, \fIeg\fR \fBhttps://raw\.githubusercontent\.com/caskroom/homebrew\-cask/f25b6babcd398abf48e33af3d887b2d00de1d661/Casks/google\-chrome\.rb\fR +. +.IP "" 0 +. +.SH "ENVIRONMENT" +Homebrew\-Cask respects many of the environment variables used by the parent command \fBbrew\fR\. Please refer to the \fBbrew\fR(1) man page for more information\. +. +.P +Environment variables specific to Homebrew\-Cask: +. +.TP +HOMEBREW_CASK_OPTS +This variable may contain any arguments normally used as options on the command\-line\. This is particularly useful to make options persistent\. For example, you might add to your \.bash_profile or \.zshenv something like: \fBexport HOMEBREW_CASK_OPTS=\'\-\-appdir=/Applications \-\-caskroom=/etc/Caskroom\'\fR\. +. +.SH "SEE ALSO" +The Homebrew\-Cask home page: \fIhttp://caskroom\.io\fR\. +. +.P +The Homebrew\-Cask GitHub page: \fIhttps://github\.com/caskroom/homebrew\-cask\fR\. +. +.P +\fBbrew\fR(1), \fBcurl\fR(1) +. +.SH "AUTHORS" +Paul Hinze and Contributors\. +. +.P +Man page format based on \fBbrew\.1\.md\fR from Homebrew\. +. +.SH "BUGS" +We still have bugs — and we are busy fixing them! If you have a problem, don’t be shy about reporting it on our GitHub issues page \fIhttps://github\.com/caskroom/homebrew\-cask/issues?state=open\fR\. +. +.P +When reporting bugs, remember that Homebrew\-Cask is an independent project from Homebrew\. Do your best to direct bug reports to the appropriate project\. If your command\-line started with \fBbrew cask\fR, bring the bug to us first! diff --git a/Library/Homebrew/cask/spec/cask/artifact/binary_spec.rb b/Library/Homebrew/cask/spec/cask/artifact/binary_spec.rb new file mode 100644 index 0000000000..81e5c9026b --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/artifact/binary_spec.rb @@ -0,0 +1,88 @@ +require "spec_helper" + +describe Hbc::Artifact::Binary do + let(:cask) { + Hbc.load("with-binary").tap do |cask| + shutup do + InstallHelper.install_without_artifacts(cask) + end + end + } + let(:expected_path) { + Hbc.binarydir.join("binary") + } + before(:each) do + Hbc.binarydir.mkpath + end + after(:each) do + FileUtils.rm expected_path if expected_path.exist? + end + + it "links the binary to the proper directory" do + shutup do + Hbc::Artifact::Binary.new(cask).install_phase + end + expect(FileHelper.valid_alias?(expected_path)).to be true + end + + it "avoids clobbering an existing binary by linking over it" do + FileUtils.touch expected_path + + shutup do + Hbc::Artifact::Binary.new(cask).install_phase + end + + expect(expected_path).to_not be :symlink? + end + + it "clobbers an existing symlink" do + expected_path.make_symlink("/tmp") + + shutup do + Hbc::Artifact::Binary.new(cask).install_phase + end + + expect(File.readlink(expected_path)).not_to eq("/tmp") + end + + it "respects --no-binaries flag" do + Hbc.no_binaries = true + + shutup do + Hbc::Artifact::Binary.new(cask).install_phase + end + + expect(expected_path.exist?).to be false + + Hbc.no_binaries = false + end + + it "creates parent directory if it doesn't exist" do + FileUtils.rmdir Hbc.binarydir + + shutup do + Hbc::Artifact::Binary.new(cask).install_phase + end + + expect(expected_path.exist?).to be true + end + + context "binary is inside an app package" do + let(:cask) { + Hbc.load("with-embedded-binary").tap do |cask| + shutup do + InstallHelper.install_without_artifacts(cask) + end + end + } + + it "links the binary to the proper directory" do + shutup do + Hbc::Artifact::App.new(cask).install_phase + Hbc::Artifact::Binary.new(cask).install_phase + end + + expect(FileHelper.valid_alias?(expected_path)).to be true + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/audit_spec.rb b/Library/Homebrew/cask/spec/cask/audit_spec.rb new file mode 100644 index 0000000000..65a1335a5c --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/audit_spec.rb @@ -0,0 +1,333 @@ +require "spec_helper" + +describe Hbc::Audit do + include AuditMatchers + include Sha256Helper + + let(:cask) { instance_double(Hbc::Cask) } + let(:download) { false } + let(:check_token_conflicts) { false } + let(:fake_system_command) { class_double(Hbc::SystemCommand) } + let(:audit) { + Hbc::Audit.new(cask, download: download, + check_token_conflicts: check_token_conflicts, + command: fake_system_command) + } + + describe "#result" do + subject { audit.result } + + context "when there are errors" do + before do + audit.add_error "bad" + end + + it { should match(%r{failed}) } + end + + context "when there are warnings" do + before do + audit.add_warning "eh" + end + + it { should match(%r{warning}) } + end + + context "when there are errors and warnings" do + before do + audit.add_error "bad" + audit.add_warning "eh" + end + + it { should match(%r{failed}) } + end + + context "when there are no errors or warnings" do + it { should match(%r{passed}) } + end + end + + describe "#run!" do + let(:cask) { Hbc.load(cask_token) } + subject { audit.run! } + + describe "required stanzas" do + %w[version sha256 url name homepage license].each do |stanza| + context "when missing #{stanza}" do + let(:cask_token) { "missing-#{stanza}" } + it { should fail_with(%r{#{stanza} stanza is required}) } + end + end + end + + describe "version checks" do + let(:error_msg) { "you should use version :latest instead of version 'latest'" } + + context "when version is 'latest'" do + let(:cask_token) { "version-latest-string" } + it { should fail_with(error_msg) } + end + + context "when version is :latest" do + let(:cask_token) { "version-latest-with-checksum" } + it { should_not fail_with(error_msg) } + end + end + + describe "sha256 checks" do + context "when version is :latest and sha256 is not :no_check" do + let(:cask_token) { "version-latest-with-checksum" } + it { should fail_with("you should use sha256 :no_check when version is :latest") } + end + + context "when sha256 is not a legal SHA-256 digest" do + let(:cask_token) { "invalid-sha256" } + it { should fail_with("sha256 string must be of 64 hexadecimal characters") } + end + + context "when sha256 is sha256 for empty string" do + let(:cask_token) { "sha256-for-empty-string" } + it { should fail_with(%r{cannot use the sha256 for an empty string}) } + end + end + + describe "appcast checks" do + context "when appcast has no sha256" do + let(:cask_token) { "appcast-missing-checkpoint" } + it { should fail_with(%r{checkpoint sha256 is required for appcast}) } + end + + context "when appcast checkpoint is not a string of 64 hexadecimal characters" do + let(:cask_token) { "appcast-invalid-checkpoint" } + it { should fail_with(%r{string must be of 64 hexadecimal characters}) } + end + + context "when appcast checkpoint is sha256 for empty string" do + let(:cask_token) { "appcast-checkpoint-sha256-for-empty-string" } + it { should fail_with(%r{cannot use the sha256 for an empty string}) } + end + + context "when appcast checkpoint is valid sha256" do + let(:cask_token) { "appcast-valid-checkpoint" } + it { should_not fail_with(%r{appcast :checkpoint}) } + end + + context "when verifying appcast HTTP code" do + let(:cask_token) { "appcast-valid-checkpoint" } + let(:download) { instance_double(Hbc::Download) } + let(:wrong_code_msg) { %r{unexpected HTTP response code} } + let(:curl_error_msg) { %r{error retrieving appcast} } + let(:fake_curl_result) { instance_double(Hbc::SystemCommand::Result) } + + before do + allow(audit).to receive(:check_appcast_checkpoint_accuracy) + allow(fake_system_command).to receive(:run).and_return(fake_curl_result) + allow(fake_curl_result).to receive(:success?).and_return(success) + end + + context "when curl succeeds" do + let(:success) { true } + + before do + allow(fake_curl_result).to receive(:stdout).and_return(stdout) + end + + context "when HTTP code is 200" do + let(:stdout) { "200" } + it { should_not warn_with(wrong_code_msg) } + end + + context "when HTTP code is not 200" do + let(:stdout) { "404" } + it { should warn_with(wrong_code_msg) } + end + end + + context "when curl fails" do + let(:success) { false } + + before do + allow(fake_curl_result).to receive(:stderr).and_return("Some curl error") + end + + it { should warn_with(curl_error_msg) } + end + end + + context "when verifying appcast checkpoint" do + let(:cask_token) { "appcast-valid-checkpoint" } + let(:download) { instance_double(Hbc::Download) } + let(:mismatch_msg) { %r{appcast checkpoint mismatch} } + let(:curl_error_msg) { %r{error retrieving appcast} } + let(:fake_curl_result) { instance_double(Hbc::SystemCommand::Result) } + let(:expected_checkpoint) { "d5b2dfbef7ea28c25f7a77cd7fa14d013d82b626db1d82e00e25822464ba19e2" } + + before do + allow(audit).to receive(:check_appcast_http_code) + allow(fake_system_command).to receive(:run).and_return(fake_curl_result) + allow(fake_curl_result).to receive(:success?).and_return(success) + end + + context "when appcast download succeeds" do + let(:success) { true } + let(:appcast_text) { instance_double(::String) } + + before do + allow(fake_curl_result).to receive(:stdout).and_return(appcast_text) + allow(appcast_text).to receive(:gsub).and_return(appcast_text) + allow(appcast_text).to receive(:end_with?).with("\n").and_return(true) + allow(Digest::SHA2).to receive(:hexdigest).and_return(actual_checkpoint) + end + + context "when appcast checkpoint is out of date" do + let(:actual_checkpoint) { random_sha256 } + it { should warn_with(mismatch_msg) } + it { should_not warn_with(curl_error_msg) } + end + + context "when appcast checkpoint is up to date" do + let(:actual_checkpoint) { expected_checkpoint } + it { should_not warn_with(mismatch_msg) } + it { should_not warn_with(curl_error_msg) } + end + end + + context "when appcast download fails" do + let(:success) { false } + + before do + allow(fake_curl_result).to receive(:stderr).and_return("Some curl error") + end + + it { should warn_with(curl_error_msg) } + end + end + end + + describe "preferred download URL formats" do + let(:warning_msg) { %r{URL format incorrect} } + + context "with incorrect SourceForge URL format" do + let(:cask_token) { "sourceforge-incorrect-url-format" } + it { should warn_with(warning_msg) } + end + + context "with correct SourceForge URL format" do + let(:cask_token) { "sourceforge-correct-url-format" } + it { should_not warn_with(warning_msg) } + end + + context "with correct SourceForge URL format for version :latest" do + let(:cask_token) { "sourceforge-version-latest-correct-url-format" } + it { should_not warn_with(warning_msg) } + end + + context "with incorrect OSDN URL format" do + let(:cask_token) { "osdn-incorrect-url-format" } + it { should warn_with(warning_msg) } + end + + context "with correct OSDN URL format" do + let(:cask_token) { "osdn-correct-url-format" } + it { should_not warn_with(warning_msg) } + end + end + + describe "generic artifact checks" do + context "with no target" do + let(:cask_token) { "generic-artifact-no-target" } + it { should fail_with(%r{target required for generic artifact}) } + end + + context "with relative target" do + let(:cask_token) { "generic-artifact-relative-target" } + it { should fail_with(%r{target must be absolute path for generic artifact}) } + end + + context "with absolute target" do + let(:cask_token) { "generic-artifact-absolute-target" } + it { should_not fail_with(%r{target required for generic artifact}) } + end + end + + describe "url checks" do + context "given a block" do + let(:cask_token) { "booby-trap" } + + context "when loading the cask" do + it "does not evaluate the block" do + expect { cask }.to_not raise_error + end + end + + context "when doing the audit" do + it "evaluates the block" do + expect(subject).to fail_with(%r{Boom}) + end + end + end + end + + describe "token conflicts" do + let(:cask_token) { "with-binary" } + let(:check_token_conflicts) { true } + + before do + expect(audit).to receive(:core_formula_names).and_return(formula_names) + end + + context "when cask token conflicts with a core formula" do + let(:formula_names) { %w[with-binary other-formula] } + it { should warn_with(%r{possible duplicate}) } + end + + context "when cask token does not conflict with a core formula" do + let(:formula_names) { %w[other-formula] } + it { should_not warn_with(%r{possible duplicate}) } + end + end + + describe "audit of downloads" do + let(:cask_token) { "with-binary" } + let(:cask) { Hbc.load(cask_token) } + let(:download) { instance_double(Hbc::Download) } + let(:verify) { class_double(Hbc::Verify).as_stubbed_const } + let(:error_msg) { "Download Failed" } + + context "when download and verification succeed" do + before do + download.expects(:perform) + verify.expects(:all) + end + + it { should_not fail_with(%r{#{error_msg}}) } + end + + context "when download fails" do + before do + download.expects(:perform).raises(StandardError.new(error_msg)) + end + + it { should fail_with(%r{#{error_msg}}) } + end + + context "when verification fails" do + before do + download.expects(:perform) + verify.expects(:all).raises(StandardError.new(error_msg)) + end + + it { should fail_with(%r{#{error_msg}}) } + end + end + + context "when an exception is raised" do + let(:cask) { instance_double(Hbc::Cask) } + before do + cask.expects(:version).raises(StandardError.new) + end + + it { should fail_with(%r{exception while auditing}) } + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/cask_spec.rb b/Library/Homebrew/cask/spec/cask/cask_spec.rb new file mode 100644 index 0000000000..f31bee023e --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/cask_spec.rb @@ -0,0 +1,25 @@ +require "spec_helper" + +describe Hbc::Cask do + let(:cask) { described_class.new("versioned-cask") } + + context "when multiple versions are installed" do + describe "#versions" do + context "and there are duplicate versions" do + it "uses the last unique version" do + allow(cask).to receive(:timestamped_versions).and_return([ + ["1.2.2", "0999"], + ["1.2.3", "1000"], + ["1.2.2", "1001"], + ]) + + expect(cask).to receive(:timestamped_versions) + expect(cask.versions).to eq([ + "1.2.3", + "1.2.2", + ]) + end + end + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/cli/cleanup_spec.rb b/Library/Homebrew/cask/spec/cask/cli/cleanup_spec.rb new file mode 100644 index 0000000000..e77576ae32 --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/cli/cleanup_spec.rb @@ -0,0 +1,68 @@ +require "spec_helper" + +describe Hbc::CLI::Cleanup do + let(:cache_location) { Pathname.new(Dir.mktmpdir).realpath } + let(:cleanup_outdated) { false } + + subject { described_class.new(cache_location, cleanup_outdated) } + + after do + cache_location.rmtree + end + + describe "cleanup!" do + it "removes cached downloads" do + cached_download = cache_location.join("SomeDownload.dmg") + FileUtils.touch(cached_download) + cleanup_size = subject.disk_cleanup_size + + expect { + subject.cleanup! + }.to output(<<-OUTPUT.undent).to_stdout + ==> Removing cached downloads + #{cached_download} + ==> This operation has freed approximately #{disk_usage_readable(cleanup_size)} of disk space. + OUTPUT + + expect(cached_download.exist?).to eq(false) + end + + it "does not removed locked files" do + cached_download = cache_location.join("SomeDownload.dmg") + FileUtils.touch(cached_download) + cleanup_size = subject.disk_cleanup_size + + File.new(cached_download).flock(File::LOCK_EX) + + expect(Hbc::Utils).to be_file_locked(cached_download) + + expect { + subject.cleanup! + }.to output(<<-OUTPUT.undent).to_stdout + ==> Removing cached downloads + skipping: #{cached_download} is locked + ==> This operation has freed approximately #{disk_usage_readable(cleanup_size)} of disk space. + OUTPUT + + expect(cached_download.exist?).to eq(true) + end + + context "when cleanup_outdated is specified" do + let(:cleanup_outdated) { true } + + it "does not remove cache files newer than 10 days old" do + cached_download = cache_location.join("SomeNewDownload.dmg") + FileUtils.touch(cached_download) + + expect { + subject.cleanup! + }.to output(<<-OUTPUT.undent).to_stdout + ==> Removing cached downloads older than 10 days old + Nothing to do + OUTPUT + + expect(cached_download.exist?).to eq(true) + end + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/cli/doctor_spec.rb b/Library/Homebrew/cask/spec/cask/cli/doctor_spec.rb new file mode 100644 index 0000000000..3c9c661501 --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/cli/doctor_spec.rb @@ -0,0 +1,16 @@ +require "spec_helper" +require "hbc/version" + +describe Hbc::CLI::Doctor do + it "displays some nice info about the environment" do + expect { + Hbc::CLI::Doctor.run + }.to output(%r{\A==> macOS Release:}).to_stdout + end + + it "raises an exception when arguments are given" do + expect { + Hbc::CLI::Doctor.run("argument") + }.to raise_error(ArgumentError) + end +end diff --git a/Library/Homebrew/cask/spec/cask/cli/style_spec.rb b/Library/Homebrew/cask/spec/cask/cli/style_spec.rb new file mode 100644 index 0000000000..0e53d62a23 --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/cli/style_spec.rb @@ -0,0 +1,224 @@ +require "English" +require "spec_helper" + +describe Hbc::CLI::Style do + let(:args) { [] } + let(:cli) { described_class.new(args) } + + around do |example| + shutup { example.run } + end + + describe ".run" do + subject { described_class.run(args) } + + before do + allow(described_class).to receive(:new).and_return(cli) + allow(cli).to receive(:run).and_return(retval) + end + + context "when rubocop succeeds" do + let(:retval) { true } + + it "exits successfully" do + subject + end + end + + context "when rubocop fails" do + let(:retval) { false } + + it "raises an exception" do + expect { subject }.to raise_error(Hbc::CaskError) + end + end + end + + describe "#run" do + subject { cli.run } + + before do + allow(cli).to receive_messages(install_rubocop: nil, + system: nil, + rubocop_args: nil, + cask_paths: nil) + allow($CHILD_STATUS).to receive(:success?).and_return(success) + end + + context "when rubocop succeeds" do + let(:success) { true } + it { is_expected.to be_truthy } + end + + context "when rubocop fails" do + let(:success) { false } + it { is_expected.to be_falsey } + end + end + + describe "#install_rubocop" do + subject { cli.install_rubocop } + + context "when installation succeeds" do + before do + allow(Homebrew).to receive(:install_gem_setup_path!) + end + + it "exits successfully" do + expect { subject }.to_not raise_error + end + end + + context "when installation fails" do + before do + allow(Homebrew).to receive(:install_gem_setup_path!).and_raise(SystemExit) + end + + it "raises an error" do + expect { subject }.to raise_error(Hbc::CaskError) + end + end + end + + describe "#cask_paths" do + subject { cli.cask_paths } + + before do + allow(cli).to receive(:cask_tokens).and_return(tokens) + end + + context "when no cask tokens are given" do + let(:tokens) { [] } + + before do + allow(Hbc).to receive(:all_tapped_cask_dirs).and_return(%w[Casks MoreCasks]) + end + + it { is_expected.to eq(%w[Casks MoreCasks]) } + end + + context "when at least one cask token is a path that exists" do + let(:tokens) { ["adium", "Casks/dropbox.rb"] } + before do + allow(File).to receive(:exist?).and_return(false, true) + end + + it "treats all tokens as paths" do + expect(subject).to eq(tokens) + end + end + + context "when no cask tokens are paths that exist" do + let(:tokens) { %w[adium dropbox] } + before do + allow(File).to receive(:exist?).and_return(false) + end + + it "tries to find paths for all tokens" do + expect(Hbc).to receive(:path).twice + subject + end + end + end + + describe "#cask_tokens" do + subject { cli.cask_tokens } + + context "when no args are given" do + let(:args) { [] } + it { is_expected.to be_empty } + end + + context "when only flags are given" do + let(:args) { ["--fix"] } + it { is_expected.to be_empty } + end + + context "when only empty args are given" do + let(:args) { ["", ""] } + it { is_expected.to be_empty } + end + + context "when a cask token is given" do + let(:args) { ["adium"] } + it { is_expected.to eq(["adium"]) } + end + + context "when multiple cask tokens are given" do + let(:args) { %w[adium dropbox] } + it { is_expected.to eq(%w[adium dropbox]) } + end + + context "when cask tokens are given with flags" do + let(:args) { ["adium", "dropbox", "--fix"] } + it { is_expected.to eq(%w[adium dropbox]) } + end + end + + describe "#rubocop_args" do + subject { cli.rubocop_args } + + before do + allow(cli).to receive(:fix?).and_return(fix) + end + + context "when fix? is true" do + let(:fix) { true } + it { is_expected.to include("--auto-correct") } + end + + context "when fix? is false" do + let(:fix) { false } + it { is_expected.not_to include("--auto-correct") } + end + end + + describe "#default_args" do + subject { cli.default_args } + let(:rubocop_config) { ".rubocop.yml" } + before do + allow(cli).to receive(:rubocop_config).and_return(rubocop_config) + end + + it { is_expected.to include("--format", "simple", "--force-exclusion", "--config", rubocop_config) } + end + + describe "#autocorrect_args" do + subject { cli.autocorrect_args } + let(:default_args) { ["--format", "simple"] } + + it "should add --auto-correct to default args" do + allow(cli).to receive(:default_args).and_return(default_args) + expect(subject).to include("--auto-correct", *default_args) + end + end + + describe "#fix?" do + subject { cli.fix? } + + context "when --fix is passed as an argument" do + let(:args) { ["adium", "--fix"] } + it { should be_truthy } + end + + context "when --correct is passed as an argument" do + let(:args) { ["adium", "--correct"] } + it { should be_truthy } + end + + context "when --auto-correct is passed as an argument" do + let(:args) { ["adium", "--auto-correct"] } + it { should be_truthy } + end + + context "when --auto-correct is misspelled as --autocorrect" do + let(:args) { ["adium", "--autocorrect"] } + it { should be_truthy } + end + + context "when no flag equivalent to --fix is passed as an argument" do + let(:args) { ["adium"] } + it { should be_falsey } + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/cli_spec.rb b/Library/Homebrew/cask/spec/cask/cli_spec.rb new file mode 100644 index 0000000000..cb21dbd25c --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/cli_spec.rb @@ -0,0 +1,63 @@ +require "spec_helper" + +describe Hbc::CLI do + it "lists the taps for Casks that show up in two taps" do + listing = Hbc::CLI.nice_listing(%w[ + caskroom/cask/adium + caskroom/cask/google-chrome + passcod/homebrew-cask/adium + ]) + + expect(listing).to eq(%w[ + caskroom/cask/adium + google-chrome + passcod/cask/adium + ]) + end + + context ".process" do + let(:noop_command) { double("CLI::Noop") } + + before do + allow(Hbc).to receive(:init) + allow(described_class).to receive(:lookup_command).with("noop").and_return(noop_command) + allow(noop_command).to receive(:run) + end + + around do |example| + shutup { example.run } + end + + it "passes `--version` along to the subcommand" do + expect(described_class).to receive(:run_command).with(noop_command, "--version") + described_class.process(%w[noop --version]) + end + + it "prints help output when subcommand receives `--help` flag" do + expect(described_class).to receive(:run_command).with("help") + described_class.process(%w[noop --help]) + expect(Hbc.help).to eq(true) + Hbc.help = false + end + + it "respects the env variable when choosing what appdir to create" do + EnvHelper.with_env_var("HOMEBREW_CASK_OPTS", "--appdir=/custom/appdir") do + expect(Hbc).to receive(:appdir=).with(Pathname("/custom/appdir")) + described_class.process("noop") + end + end + + it "respects the env variable when choosing a non-default Caskroom location" do + EnvHelper.with_env_var "HOMEBREW_CASK_OPTS", "--caskroom=/custom/caskdir" do + expect(Hbc).to receive(:caskroom=).with(Pathname("/custom/caskdir")) + described_class.process("noop") + end + end + + it "exits with a status of 1 when something goes wrong" do + allow(described_class).to receive(:lookup_command).and_raise(Hbc::CaskError) + expect(described_class).to receive(:exit).with(1) + described_class.process("noop") + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/download_strategy_spec.rb b/Library/Homebrew/cask/spec/cask/download_strategy_spec.rb new file mode 100644 index 0000000000..9b1c4d8e3c --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/download_strategy_spec.rb @@ -0,0 +1,308 @@ +require "spec_helper" + +describe "download strategies" do + let(:url) { "http://example.com/cask.dmg" } + let(:url_options) { Hash.new } + let(:cask) { + instance_double(Hbc::Cask, token: "some-cask", + url: Hbc::URL.new(url, url_options), + version: "1.2.3.4") + } + + describe Hbc::CurlDownloadStrategy do + let(:downloader) { Hbc::CurlDownloadStrategy.new(cask) } + + before do + allow(downloader.temporary_path).to receive(:rename) + end + + it "properly assigns a name and uri based on the Cask" do + expect(downloader.name).to eq("some-cask") + expect(downloader.url).to eq("http://example.com/cask.dmg") + expect(downloader.version.to_s).to eq("1.2.3.4") + end + + it "calls curl with default arguments for a simple Cask" do + allow(downloader).to receive(:curl) + + shutup do + downloader.fetch + end + + expect(downloader).to have_received(:curl).with( + cask.url.to_s, + "-C", 0, + "-o", kind_of(Pathname) + ) + end + + context "with an explicit user agent" do + let(:url_options) { { user_agent: "Mozilla/25.0.1" } } + + it "adds the appropriate curl args" do + curl_args = [] + allow(downloader).to receive(:curl) { |*args| curl_args = args } + + shutup do + downloader.fetch + end + + expect(curl_args.each_cons(2)).to include(["-A", "Mozilla/25.0.1"]) + end + end + + context "with a generalized fake user agent" do + let(:url_options) { { user_agent: :fake } } + + it "adds the appropriate curl args" do + curl_args = [] + allow(downloader).to receive(:curl) { |*args| curl_args = args } + + shutup do + downloader.fetch + end + + expect(curl_args.each_cons(2)).to include(["-A", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10) http://caskroom.io"]) + end + end + + context "with cookies set" do + let(:url_options) { + { + cookies: { + coo: "kie", + mon: "ster", + }, + } + } + + it "adds curl args for cookies" do + curl_args = [] + allow(downloader).to receive(:curl) { |*args| curl_args = args } + + shutup do + downloader.fetch + end + + expect(curl_args.each_cons(2)).to include(["-b", "coo=kie;mon=ster"]) + end + end + + context "with referer set" do + let(:url_options) { { referer: "http://somehost/also" } } + + it "adds curl args for referer" do + curl_args = [] + allow(downloader).to receive(:curl) { |*args| curl_args = args } + + shutup do + downloader.fetch + end + + expect(curl_args.each_cons(2)).to include(["-e", "http://somehost/also"]) + end + end + end + + describe Hbc::CurlPostDownloadStrategy do + let(:downloader) { Hbc::CurlPostDownloadStrategy.new(cask) } + + before do + allow(downloader.temporary_path).to receive(:rename) + end + + context "with :using and :data specified" do + let(:url_options) { + { + using: :post, + data: { + form: "data", + is: "good", + }, + } + } + + it "adds curl args for post arguments" do + curl_args = [] + allow(downloader).to receive(:curl) { |*args| curl_args = args } + + shutup do + downloader.fetch + end + + expect(curl_args.each_cons(2)).to include(["-d", "form=data"]) + expect(curl_args.each_cons(2)).to include(["-d", "is=good"]) + end + end + + context "with :using but no :data" do + let(:url_options) { { using: :post } } + + it "adds curl args for a POST request" do + curl_args = [] + allow(downloader).to receive(:curl) { |*args| curl_args = args } + + shutup do + downloader.fetch + end + + expect(curl_args.each_cons(2)).to include(["-X", "POST"]) + end + end + end + + describe Hbc::SubversionDownloadStrategy do + let(:url_options) { { using: :svn } } + let(:fake_system_command) { class_double(Hbc::SystemCommand) } + let(:downloader) { Hbc::SubversionDownloadStrategy.new(cask, fake_system_command) } + before do + allow(fake_system_command).to receive(:run!) + end + + it "returns a tarball path on fetch" do + allow(downloader).to receive(:compress) + allow(downloader).to receive(:fetch_repo) + + retval = shutup { downloader.fetch } + + expect(retval).to equal(downloader.tarball_path) + end + + it "calls fetch_repo with default arguments for a simple Cask" do + allow(downloader).to receive(:compress) + allow(downloader).to receive(:fetch_repo) + + shutup do + downloader.fetch + end + + expect(downloader).to have_received(:fetch_repo).with( + downloader.cached_location, + cask.url.to_s + ) + end + + it "calls svn with default arguments for a simple Cask" do + allow(downloader).to receive(:compress) + + shutup do + downloader.fetch + end + + expect(fake_system_command).to have_received(:run!).with( + "/usr/bin/svn", + hash_including(args: [ + "checkout", + "--force", + "--config-option", + "config:miscellany:use-commit-times=yes", + cask.url.to_s, + downloader.cached_location, + ]) + ) + end + + context "with trust_cert set on the URL" do + let(:url_options) { + { + using: :svn, + trust_cert: true, + } + } + + it "adds svn arguments for :trust_cert" do + allow(downloader).to receive(:compress) + + shutup do + downloader.fetch + end + + expect(fake_system_command).to have_received(:run!).with( + "/usr/bin/svn", + hash_including(args: [ + "checkout", + "--force", + "--config-option", + "config:miscellany:use-commit-times=yes", + "--trust-server-cert", + "--non-interactive", + cask.url.to_s, + downloader.cached_location, + ]) + ) + end + end + + context "with :revision set on url" do + let(:url_options) { + { + using: :svn, + revision: "10", + } + } + + it "adds svn arguments for :revision" do + allow(downloader).to receive(:compress) + + shutup do + downloader.fetch + end + + expect(fake_system_command).to have_received(:run!).with( + "/usr/bin/svn", + hash_including(args: [ + "checkout", + "--force", + "--config-option", + "config:miscellany:use-commit-times=yes", + cask.url.to_s, + downloader.cached_location, + "-r", + "10", + ]) + ) + end + end + + it "runs tar to serialize svn downloads" do + # sneaky stub to remake the directory, since homebrew code removes it + # before tar is called + allow(downloader).to receive(:fetch_repo) { + downloader.cached_location.mkdir + } + + shutup do + downloader.fetch + end + + expect(fake_system_command).to have_received(:run!).with( + "/usr/bin/tar", + hash_including(args: [ + '-s/^\\.//', + "--exclude", + ".svn", + "-cf", + downloader.tarball_path, + "--", + ".", + ]) + ) + end + end + + # does not work yet, because (for unknown reasons), the tar command + # returns an error code when running under the test suite + # it 'creates a tarball matching the expected checksum' do + # cask = Hbc.load('svn-download-check-cask') + # downloader = Hbc::SubversionDownloadStrategy.new(cask) + # # special mocking required for tar to have something to work with + # def downloader.fetch_repo(target, url, revision = nil, ignore_externals=false) + # target.mkpath + # FileUtils.touch(target.join('empty_file.txt')) + # File.utime(1000,1000,target.join('empty_file.txt')) + # end + # expect(shutup { downloader.fetch }).to equal(downloader.tarball_path) + # d = Hbc::Download.new(cask) + # d.send(:_check_sums, downloader.tarball_path, cask.sums) + # end +end diff --git a/Library/Homebrew/cask/spec/cask/dsl/stanza_proxy_spec.rb b/Library/Homebrew/cask/spec/cask/dsl/stanza_proxy_spec.rb new file mode 100644 index 0000000000..8516f0d382 --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/dsl/stanza_proxy_spec.rb @@ -0,0 +1,32 @@ +describe Hbc::DSL::StanzaProxy do + let(:stanza_proxy) { + described_class.new(Array) { %i{foo bar cake} } + } + + subject { stanza_proxy } + it { is_expected.to be_a_proxy } + it { is_expected.to respond_to(:pop) } + its(:pop) { is_expected.to eq(:cake) } + its(:type) { is_expected.to eq(Array) } + its(:to_s) { is_expected.to eq("[:foo, :bar, :cake]") } + + describe "when initialized" do + let(:initializing) { + proc { |b| described_class.new(Array, &b) } + } + + it "does not evaluate the block" do + expect(&initializing).not_to yield_control + end + end + + describe "when receiving a message" do + let(:receiving_a_message) { + proc { |b| described_class.new(Array, &b).to_s } + } + + it "evaluates the block" do + expect(&receiving_a_message).to yield_with_no_args + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/dsl/version_spec.rb b/Library/Homebrew/cask/spec/cask/dsl/version_spec.rb new file mode 100644 index 0000000000..04dcd06ca6 --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/dsl/version_spec.rb @@ -0,0 +1,227 @@ +require "spec_helper" + +describe Hbc::DSL::Version do + include ExpectationsHashHelper + + let(:version) { described_class.new(raw_version) } + + shared_examples "version equality" do + let(:raw_version) { "1.2.3" } + + context "when other is nil" do + let(:other) { nil } + it { should == false } + end + + context "when other is a String" do + context "when other == self.raw_version" do + let(:other) { "1.2.3" } + it { should == true } + end + + context "when other != self.raw_version" do + let(:other) { "1.2.3.4" } + it { should == false } + end + end + + context "when other is a #{described_class}" do + context "when other.raw_version == self.raw_version" do + let(:other) { described_class.new("1.2.3") } + it { should == true } + end + + context "when other.raw_version != self.raw_version" do + let(:other) { described_class.new("1.2.3.4") } + it { should == false } + end + end + end + + describe "#==" do + subject { version == other } + include_examples "version equality" + end + + describe "#eql?" do + subject { version.eql?(other) } + include_examples "version equality" + end + + shared_examples "version expectations hash" do |method, hash| + subject { version.send(method) } + include_examples "expectations hash", :raw_version, + { :latest => "latest", + "latest" => "latest", + "" => "", + nil => "" }.merge(hash) + end + + describe "#latest?" do + include_examples "version expectations hash", :latest?, + :latest => true, + "latest" => true, + "" => false, + nil => false, + "1.2.3" => false + end + + describe "string manipulation helpers" do + describe "#major" do + include_examples "version expectations hash", :major, + "1" => "1", + "1.2" => "1", + "1.2.3" => "1", + "1.2.3_4-5" => "1" + end + + describe "#minor" do + include_examples "version expectations hash", :minor, + "1" => "", + "1.2" => "2", + "1.2.3" => "2", + "1.2.3_4-5" => "2" + end + + describe "#patch" do + include_examples "version expectations hash", :patch, + "1" => "", + "1.2" => "", + "1.2.3" => "3", + "1.2.3_4-5" => "3" + end + + describe "#major_minor" do + include_examples "version expectations hash", :major_minor, + "1" => "1", + "1.2" => "1.2", + "1.2.3" => "1.2", + "1.2.3_4-5" => "1.2" + end + + describe "#major_minor_patch" do + include_examples "version expectations hash", :major_minor_patch, + "1" => "1", + "1.2" => "1.2", + "1.2.3" => "1.2.3", + "1.2.3_4-5" => "1.2.3" + end + + describe "#before_comma" do + include_examples "version expectations hash", :before_comma, + "1.2.3" => "1.2.3", + "1.2.3," => "1.2.3", + ",abc" => "", + "1.2.3,abc" => "1.2.3" + end + + describe "#after_comma" do + include_examples "version expectations hash", :after_comma, + "1.2.3" => "", + "1.2.3," => "", + ",abc" => "abc", + "1.2.3,abc" => "abc" + end + + describe "#before_colon" do + include_examples "version expectations hash", :before_colon, + "1.2.3" => "1.2.3", + "1.2.3:" => "1.2.3", + ":abc" => "", + "1.2.3:abc" => "1.2.3" + end + + describe "#after_colon" do + include_examples "version expectations hash", :after_colon, + "1.2.3" => "", + "1.2.3:" => "", + ":abc" => "abc", + "1.2.3:abc" => "abc" + end + + describe "#dots_to_hyphens" do + include_examples "version expectations hash", :dots_to_hyphens, + "1.2.3_4-5" => "1-2-3_4-5" + end + + describe "#dots_to_underscores" do + include_examples "version expectations hash", :dots_to_underscores, + "1.2.3_4-5" => "1_2_3_4-5" + end + + describe "#dots_to_slashes" do + include_examples "version expectations hash", :dots_to_slashes, + "1.2.3_4-5" => "1/2/3_4-5" + end + + describe "#hyphens_to_dots" do + include_examples "version expectations hash", :hyphens_to_dots, + "1.2.3_4-5" => "1.2.3_4.5" + end + + describe "#hyphens_to_underscores" do + include_examples "version expectations hash", :hyphens_to_underscores, + "1.2.3_4-5" => "1.2.3_4_5" + end + + describe "#hyphens_to_slashes" do + include_examples "version expectations hash", :hyphens_to_slashes, + "1.2.3_4-5" => "1.2.3_4/5" + end + + describe "#underscores_to_dots" do + include_examples "version expectations hash", :underscores_to_dots, + "1.2.3_4-5" => "1.2.3.4-5" + end + + describe "#underscores_to_hyphens" do + include_examples "version expectations hash", :underscores_to_hyphens, + "1.2.3_4-5" => "1.2.3-4-5" + end + + describe "#underscores_to_slashes" do + include_examples "version expectations hash", :underscores_to_slashes, + "1.2.3_4-5" => "1.2.3/4-5" + end + + describe "#slashes_to_dots" do + include_examples "version expectations hash", :slashes_to_dots, + "1.2.3/abc" => "1.2.3.abc" + end + + describe "#slashes_to_hyphens" do + include_examples "version expectations hash", :slashes_to_hyphens, + "1.2.3/abc" => "1.2.3-abc" + end + + describe "#slashes_to_underscores" do + include_examples "version expectations hash", :slashes_to_underscores, + "1.2.3/abc" => "1.2.3_abc" + end + + describe "#no_dots" do + include_examples "version expectations hash", :no_dots, + "1.2.3_4-5" => "123_4-5" + end + + describe "#no_hyphens" do + include_examples "version expectations hash", :no_hyphens, + "1.2.3_4-5" => "1.2.3_45" + end + + describe "#no_underscores" do + include_examples "version expectations hash", :no_underscores, + "1.2.3_4-5" => "1.2.34-5" + end + + describe "#no_slashes" do + include_examples "version expectations hash", :no_slashes, + "1.2.3/abc" => "1.2.3abc" + end + + describe "#no_dividers" do + include_examples "version expectations hash", :no_dividers, + "1.2.3_4-5" => "12345" + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/macos_spec.rb b/Library/Homebrew/cask/spec/cask/macos_spec.rb new file mode 100644 index 0000000000..902c80d7a2 --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/macos_spec.rb @@ -0,0 +1,69 @@ +require "spec_helper" + +describe MacOS do + it "says '/' is undeletable" do + expect(MacOS).to be_undeletable( + "/" + ) + expect(MacOS).to be_undeletable( + "/." + ) + expect(MacOS).to be_undeletable( + "/usr/local/Library/Taps/../../../.." + ) + end + + it "says '/Applications' is undeletable" do + expect(MacOS).to be_undeletable( + "/Applications" + ) + expect(MacOS).to be_undeletable( + "/Applications/" + ) + expect(MacOS).to be_undeletable( + "/Applications/." + ) + expect(MacOS).to be_undeletable( + "/Applications/Mail.app/.." + ) + end + + it "says the home directory is undeletable" do + expect(MacOS).to be_undeletable( + Dir.home + ) + expect(MacOS).to be_undeletable( + "#{Dir.home}/" + ) + expect(MacOS).to be_undeletable( + "#{Dir.home}/Documents/.." + ) + end + + it "says the user library directory is undeletable" do + expect(MacOS).to be_undeletable( + "#{Dir.home}/Library" + ) + expect(MacOS).to be_undeletable( + "#{Dir.home}/Library/" + ) + expect(MacOS).to be_undeletable( + "#{Dir.home}/Library/." + ) + expect(MacOS).to be_undeletable( + "#{Dir.home}/Library/Preferences/.." + ) + end + + it "says '/Applications/.app' is deletable" do + expect(MacOS).not_to be_undeletable( + "/Applications/.app" + ) + end + + it "says '/Applications/SnakeOil Professional.app' is deletable" do + expect(MacOS).not_to be_undeletable( + "/Applications/SnakeOil Professional.app" + ) + end +end diff --git a/Library/Homebrew/cask/spec/cask/scopes_spec.rb b/Library/Homebrew/cask/spec/cask/scopes_spec.rb new file mode 100644 index 0000000000..0e592c9909 --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/scopes_spec.rb @@ -0,0 +1,47 @@ +require "spec_helper" + +describe Hbc::Scopes do + describe "installed" do + let(:fake_caskroom) { Pathname(Dir.mktmpdir) } + + before do + allow(Hbc).to receive(:caskroom) { fake_caskroom } + end + + after do + fake_caskroom.rmtree + end + + it "returns a list installed Casks by loading Casks for all the dirs that exist in the caskroom" do + allow(Hbc).to receive(:load) { |token| "loaded-#{token}" } + + fake_caskroom.join("cask-bar").mkdir + fake_caskroom.join("cask-foo").mkdir + + installed_casks = Hbc.installed + + expect(Hbc).to have_received(:load).with("cask-bar") + expect(Hbc).to have_received(:load).with("cask-foo") + expect(installed_casks).to eq(%w[ + loaded-cask-bar + loaded-cask-foo + ]) + end + + it "optimizes performance by resolving to a fully qualified path before calling Hbc.load" do + fake_tapped_cask_dir = Pathname(Dir.mktmpdir).join("Casks") + absolute_path_to_cask = fake_tapped_cask_dir.join("some-cask.rb") + + allow(Hbc).to receive(:load) + allow(Hbc).to receive(:all_tapped_cask_dirs) { [fake_tapped_cask_dir] } + + fake_caskroom.join("some-cask").mkdir + fake_tapped_cask_dir.mkdir + FileUtils.touch(absolute_path_to_cask) + + Hbc.installed + + expect(Hbc).to have_received(:load).with(absolute_path_to_cask) + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/system_command_spec.rb b/Library/Homebrew/cask/spec/cask/system_command_spec.rb new file mode 100644 index 0000000000..c291023057 --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/system_command_spec.rb @@ -0,0 +1,141 @@ +require "spec_helper" + +describe Hbc::SystemCommand do + describe "when the exit code is 0" do + describe "its result" do + subject { described_class.run("/usr/bin/true") } + + it { is_expected.to be_a_success } + its(:exit_status) { is_expected.to eq(0) } + end + end + + describe "when the exit code is 1" do + let(:command) { "/usr/bin/false" } + + describe "and the command must succeed" do + it "throws an error" do + expect { + described_class.run!(command) + }.to raise_error(Hbc::CaskCommandFailedError) + end + end + + describe "and the command does not have to succeed" do + describe "its result" do + subject { described_class.run(command) } + + it { is_expected.not_to be_a_success } + its(:exit_status) { is_expected.to eq(1) } + end + end + end + + describe "given a pathname" do + let(:command) { "/bin/ls" } + let(:path) { Pathname(Dir.mktmpdir) } + + before do + FileUtils.touch(path.join("somefile")) + end + + describe "its result" do + subject { described_class.run(command, args: [path]) } + + it { is_expected.to be_a_success } + its(:stdout) { is_expected.to eq("somefile\n") } + end + end + + describe "with both STDOUT and STDERR output from upstream" do + let(:command) { "/bin/bash" } + let(:options) { + { args: [ + "-c", + "for i in $(seq 1 2 5); do echo $i; echo $(($i + 1)) >&2; done", + ] } + } + + shared_examples "it returns '1 2 3 4 5 6'" do + describe "its result" do + subject { shutup { described_class.run(command, options) } } + + it { is_expected.to be_a_success } + its(:stdout) { is_expected.to eq([1, 3, 5, nil].join("\n")) } + its(:stderr) { is_expected.to eq([2, 4, 6, nil].join("\n")) } + end + end + + describe "with default options" do + it "echoes only STDERR" do + expected = [2, 4, 6].map { |i| "==> #{i}\n" }.join("") + expect { + described_class.run(command, options) + }.to output(expected).to_stdout + end + + include_examples("it returns '1 2 3 4 5 6'") + end + + describe "with print_stdout" do + before do + options.merge!(print_stdout: true) + end + + it "echoes both STDOUT and STDERR" do + (1..6).each do |i| + expect { + described_class.run(command, options) + }.to output(%r{==> #{ i }}).to_stdout + end + end + + include_examples("it returns '1 2 3 4 5 6'") + end + + describe "without print_stderr" do + before do + options.merge!(print_stderr: false) + end + + it "echoes nothing" do + expect { + described_class.run(command, options) + }.to output("").to_stdout + end + + include_examples("it returns '1 2 3 4 5 6'") + end + + describe "with print_stdout but without print_stderr" do + before do + options.merge!(print_stdout: true, print_stderr: false) + end + + it "echoes only STDOUT" do + expected = [1, 3, 5].map { |i| "==> #{i}\n" }.join("") + expect { + described_class.run(command, options) + }.to output(expected).to_stdout + end + + include_examples("it returns '1 2 3 4 5 6'") + end + end + + describe "with a very long STDERR output" do + let(:command) { "/bin/bash" } + let(:options) { + { args: [ + "-c", + "for i in $(seq 1 2 100000); do echo $i; echo $(($i + 1)) >&2; done", + ] } + } + + it "returns without deadlocking" do + wait(15).for { + shutup { described_class.run(command, options) } + }.to be_a_success + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/underscore_supporting_uri_spec.rb b/Library/Homebrew/cask/spec/cask/underscore_supporting_uri_spec.rb new file mode 100644 index 0000000000..a8756fde0c --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/underscore_supporting_uri_spec.rb @@ -0,0 +1,16 @@ +require "spec_helper" + +describe Hbc::UnderscoreSupportingURI do + describe "parse" do + it "works like normal on normal URLs" do + uri = Hbc::UnderscoreSupportingURI.parse("http://example.com/TestCask.dmg") + expect(uri).to eq(URI("http://example.com/TestCask.dmg")) + end + + it "works just fine on URIs with underscores" do + uri = Hbc::UnderscoreSupportingURI.parse("http://dl_dir.qq.com/qqfile/qq/QQforMac/QQ_V3.0.0.dmg") + expect(uri.host).to include("_") + expect(uri.to_s).to eq("http://dl_dir.qq.com/qqfile/qq/QQforMac/QQ_V3.0.0.dmg") + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/verify/checksum_spec.rb b/Library/Homebrew/cask/spec/cask/verify/checksum_spec.rb new file mode 100644 index 0000000000..7209556c9e --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/verify/checksum_spec.rb @@ -0,0 +1,95 @@ +require "spec_helper" + +describe Hbc::Verify::Checksum do + include Sha256Helper + + let(:cask) { double("cask") } + let(:downloaded_path) { double("downloaded_path") } + let(:verification) { described_class.new(cask, downloaded_path) } + + before do + allow(cask).to receive(:sha256).and_return(sha256) + end + + around do |example| + shutup { example.run } + end + + describe ".me?" do + subject { described_class.me?(cask) } + + context "sha256 is :no_check" do + let(:sha256) { :no_check } + + it { should == false } + end + + context "sha256 is nil" do + let(:sha256) { nil } + + it { should == true } + end + + context "sha256 is empty" do + let(:sha256) { "" } + + it { should == true } + end + + context "sha256 is a valid shasum" do + let(:sha256) { random_sha256 } + + it { should == true } + end + end + + describe "#verify" do + subject { verification.verify } + + let(:computed) { random_sha256 } + + before do + allow(verification).to receive(:computed).and_return(computed) + end + + context "sha256 matches computed" do + let(:sha256) { computed } + + it "does not raise an error" do + expect { subject }.to_not raise_error + end + end + + context "sha256 is :no_check" do + let(:sha256) { :no_check } + + it "does not raise an error" do + expect { subject }.to_not raise_error + end + end + + context "sha256 does not match computed" do + let(:sha256) { random_sha256 } + + it "raises an error" do + expect { subject }.to raise_error(Hbc::CaskSha256MismatchError) + end + end + + context "sha256 is nil" do + let(:sha256) { nil } + + it "raises an error" do + expect { subject }.to raise_error(Hbc::CaskSha256MissingError) + end + end + + context "sha256 is empty" do + let(:sha256) { "" } + + it "raises an error" do + expect { subject }.to raise_error(Hbc::CaskSha256MissingError) + end + end + end +end diff --git a/Library/Homebrew/cask/spec/cask/verify_spec.rb b/Library/Homebrew/cask/spec/cask/verify_spec.rb new file mode 100644 index 0000000000..7df77fea1d --- /dev/null +++ b/Library/Homebrew/cask/spec/cask/verify_spec.rb @@ -0,0 +1,65 @@ +require "spec_helper" + +describe Hbc::Verify do + let(:cask) { double("cask") } + + let(:verification_classes) { + [ + applicable_verification_class, + inapplicable_verification_class, + ] + } + + let(:applicable_verification_class) { + double("applicable_verification_class", me?: true) + } + + let(:inapplicable_verification_class) { + double("inapplicable_verification_class", me?: false) + } + + before do + allow(described_class).to receive(:verifications) + .and_return(verification_classes) + end + + describe ".for_cask" do + subject { described_class.for_cask(cask) } + + it "checks applicability of each verification" do + verification_classes.each do |verify_class| + expect(verify_class).to receive(:me?).with(cask) + end + subject + end + + it "includes applicable verifications" do + expect(subject).to include(applicable_verification_class) + end + + it "excludes inapplicable verifications" do + expect(subject).to_not include(inapplicable_verification_class) + end + end + + describe ".all" do + let(:downloaded_path) { double("downloaded_path") } + let(:applicable_verification) { double("applicable_verification") } + let(:inapplicable_verification) { double("inapplicable_verification") } + + subject { described_class.all(cask, downloaded_path) } + + before do + allow(applicable_verification_class).to receive(:new) + .and_return(applicable_verification) + allow(inapplicable_verification_class).to receive(:new) + .and_return(inapplicable_verification) + end + + it "runs only applicable verifications" do + expect(applicable_verification).to receive(:verify) + expect(inapplicable_verification).to_not receive(:verify) + subject + end + end +end diff --git a/Library/Homebrew/cask/spec/spec_helper.rb b/Library/Homebrew/cask/spec/spec_helper.rb new file mode 100644 index 0000000000..e86e200274 --- /dev/null +++ b/Library/Homebrew/cask/spec/spec_helper.rb @@ -0,0 +1,67 @@ +require "pathname" +require "rspec/its" +require "rspec/wait" + +if ENV["COVERAGE"] + require "coveralls" + Coveralls.wear_merged! +end + +project_root = Pathname.new(File.expand_path("../..", __FILE__)) + +# add Homebrew to load path +$LOAD_PATH.unshift(File.expand_path("#{ENV['HOMEBREW_REPOSITORY']}/Library/Homebrew")) + +require "global" +require "extend/pathname" + +# add Homebrew-Cask to load path +$LOAD_PATH.push(project_root.join("lib").to_s) + +# force some environment variables +ENV["HOMEBREW_NO_EMOJI"] = "1" +ENV["HOMEBREW_CASK_OPTS"] = nil + +Dir["#{project_root}/spec/support/*.rb"].each(&method(:require)) + +# from Homebrew. Provides expects method. +require "mocha/api" + +require "hbc" + +class Hbc::TestCask < Hbc::Cask; end + +TEST_TMPDIR = Dir.mktmpdir("homebrew_cask_tests") +at_exit do + FileUtils.remove_entry(TEST_TMPDIR) +end + +# override Homebrew locations +Hbc.homebrew_prefix = Pathname.new(TEST_TMPDIR).join("prefix") +Hbc.homebrew_repository = Hbc.homebrew_prefix +Hbc.binarydir = Hbc.homebrew_prefix.join("binarydir", "bin") +Hbc.appdir = Pathname.new(TEST_TMPDIR).join("appdir") + +# Override Tap::TAP_DIRECTORY to use our test Tap directory. +class Tap + send(:remove_const, :TAP_DIRECTORY) + TAP_DIRECTORY = Hbc.homebrew_repository.join("Library", "Taps") +end + +Hbc.default_tap = Tap.fetch("caskroom", "speccasks") +Hbc.default_tap.path.dirname.mkpath + +# also jack in some test Casks +FileUtils.ln_s project_root.join("spec", "support"), Tap::TAP_DIRECTORY.join("caskroom", "homebrew-speccasks") + +# create cache directory +Hbc.homebrew_cache = Pathname.new(TEST_TMPDIR).join("cache") +Hbc.cache.mkpath + +# our own testy caskroom +Hbc.caskroom = Hbc.homebrew_prefix.join("TestCaskroom") + +RSpec.configure do |config| + config.order = :random + config.include ShutupHelper +end diff --git a/Library/Homebrew/cask/spec/support/Casks/.rubocop.yml b/Library/Homebrew/cask/spec/support/Casks/.rubocop.yml new file mode 120000 index 0000000000..ee5c2b9485 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/.rubocop.yml @@ -0,0 +1 @@ +../../../Casks/.rubocop.yml \ No newline at end of file diff --git a/Library/Homebrew/cask/spec/support/Casks/appcast-checkpoint-sha256-for-empty-string.rb b/Library/Homebrew/cask/spec/support/Casks/appcast-checkpoint-sha256-for-empty-string.rb new file mode 100644 index 0000000000..f40f244f29 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/appcast-checkpoint-sha256-for-empty-string.rb @@ -0,0 +1,4 @@ +test_cask 'appcast-checkpoint-sha256-for-empty-string' do + appcast 'http://localhost/appcast.xml', + checkpoint: 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/appcast-invalid-checkpoint.rb b/Library/Homebrew/cask/spec/support/Casks/appcast-invalid-checkpoint.rb new file mode 100644 index 0000000000..e182c23891 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/appcast-invalid-checkpoint.rb @@ -0,0 +1,4 @@ +test_cask 'appcast-invalid-checkpoint' do + appcast 'http://localhost/appcast.xml', + checkpoint: 'not a valid shasum' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/appcast-missing-checkpoint.rb b/Library/Homebrew/cask/spec/support/Casks/appcast-missing-checkpoint.rb new file mode 100644 index 0000000000..5ab2c06652 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/appcast-missing-checkpoint.rb @@ -0,0 +1,3 @@ +test_cask 'appcast-missing-checkpoint' do + appcast 'http://localhost/appcast.xml' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/appcast-valid-checkpoint.rb b/Library/Homebrew/cask/spec/support/Casks/appcast-valid-checkpoint.rb new file mode 100644 index 0000000000..96d7edbba0 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/appcast-valid-checkpoint.rb @@ -0,0 +1,4 @@ +test_cask 'appcast-valid-checkpoint' do + appcast 'http://localhost/appcast.xml', + checkpoint: 'd5b2dfbef7ea28c25f7a77cd7fa14d013d82b626db1d82e00e25822464ba19e2' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/booby-trap.rb b/Library/Homebrew/cask/spec/support/Casks/booby-trap.rb new file mode 100644 index 0000000000..21bd97952d --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/booby-trap.rb @@ -0,0 +1,8 @@ +cask 'booby-trap' do + version '0.0.7' + + url do + # to be lazily evaluated + fail 'Boom' + end +end diff --git a/Library/Homebrew/cask/spec/support/Casks/generic-artifact-absolute-target.rb b/Library/Homebrew/cask/spec/support/Casks/generic-artifact-absolute-target.rb new file mode 100644 index 0000000000..475fb055fb --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/generic-artifact-absolute-target.rb @@ -0,0 +1,3 @@ +test_cask 'generic-artifact-absolute-target' do + artifact 'Caffeine.app', target: "#{Hbc.appdir}/Caffeine.app" +end diff --git a/Library/Homebrew/cask/spec/support/Casks/generic-artifact-no-target.rb b/Library/Homebrew/cask/spec/support/Casks/generic-artifact-no-target.rb new file mode 100644 index 0000000000..f7657dbb5a --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/generic-artifact-no-target.rb @@ -0,0 +1,3 @@ +test_cask 'generic-artifact-no-target' do + artifact 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/generic-artifact-relative-target.rb b/Library/Homebrew/cask/spec/support/Casks/generic-artifact-relative-target.rb new file mode 100644 index 0000000000..f97441751a --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/generic-artifact-relative-target.rb @@ -0,0 +1,3 @@ +test_cask 'generic-artifact-relative-target' do + artifact 'Caffeine.app', target: 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/invalid-sha256.rb b/Library/Homebrew/cask/spec/support/Casks/invalid-sha256.rb new file mode 100644 index 0000000000..aac00f4952 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/invalid-sha256.rb @@ -0,0 +1,4 @@ +test_cask 'invalid-sha256' do + version '1.2.3' + sha256 'not a valid shasum' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/missing-homepage.rb b/Library/Homebrew/cask/spec/support/Casks/missing-homepage.rb new file mode 100644 index 0000000000..ff924541d2 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/missing-homepage.rb @@ -0,0 +1,5 @@ +test_cask 'missing-homepage' do + version '1.2.3' + + url 'http://localhost/something.dmg' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/missing-license.rb b/Library/Homebrew/cask/spec/support/Casks/missing-license.rb new file mode 100644 index 0000000000..30f3791c77 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/missing-license.rb @@ -0,0 +1,5 @@ +test_cask 'missing-license' do + version '1.2.3' + + url 'http://localhost/something.dmg' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/missing-name.rb b/Library/Homebrew/cask/spec/support/Casks/missing-name.rb new file mode 100644 index 0000000000..a5265b3791 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/missing-name.rb @@ -0,0 +1,5 @@ +test_cask 'missing-name' do + version '1.2.3' + + url 'http://localhost/something.dmg' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/missing-sha256.rb b/Library/Homebrew/cask/spec/support/Casks/missing-sha256.rb new file mode 100644 index 0000000000..7f80279076 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/missing-sha256.rb @@ -0,0 +1,5 @@ +test_cask 'missing-sha256' do + version '1.2.3' + + url 'http://localhost/something.dmg' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/missing-url.rb b/Library/Homebrew/cask/spec/support/Casks/missing-url.rb new file mode 100644 index 0000000000..1b3e76b9cc --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/missing-url.rb @@ -0,0 +1,5 @@ +test_cask 'missing-url' do + version '1.2.3' + + homepage 'http://example.com' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/missing-version.rb b/Library/Homebrew/cask/spec/support/Casks/missing-version.rb new file mode 100644 index 0000000000..da2160bceb --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/missing-version.rb @@ -0,0 +1,4 @@ +test_cask 'missing-version' do + url 'http://localhost/something.dmg' + homepage 'http://example.com' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/osdn-correct-url-format.rb b/Library/Homebrew/cask/spec/support/Casks/osdn-correct-url-format.rb new file mode 100644 index 0000000000..da6ff0fcd2 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/osdn-correct-url-format.rb @@ -0,0 +1,6 @@ +test_cask 'osdn-correct-url-format' do + version '1.2.3' + + url 'http://user.dl.osdn.jp/something/id/Something-1.2.3.dmg' + homepage 'http://osdn.jp/projects/something/' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/osdn-incorrect-url-format.rb b/Library/Homebrew/cask/spec/support/Casks/osdn-incorrect-url-format.rb new file mode 100644 index 0000000000..8400159a11 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/osdn-incorrect-url-format.rb @@ -0,0 +1,6 @@ +test_cask 'osdn-incorrect-url-format' do + version '1.2.3' + + url 'http://osdn.jp/projects/something/files/Something-1.2.3.dmg/download' + homepage 'http://osdn.jp/projects/something/' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/sha256-for-empty-string.rb b/Library/Homebrew/cask/spec/support/Casks/sha256-for-empty-string.rb new file mode 100644 index 0000000000..b97764071f --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/sha256-for-empty-string.rb @@ -0,0 +1,4 @@ +test_cask 'sha256-for-empty-string' do + version '1.2.3' + sha256 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/sourceforge-correct-url-format.rb b/Library/Homebrew/cask/spec/support/Casks/sourceforge-correct-url-format.rb new file mode 100644 index 0000000000..41ef73500c --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/sourceforge-correct-url-format.rb @@ -0,0 +1,6 @@ +test_cask 'sourceforge-correct-url-format' do + version '1.2.3' + + url 'https://downloads.sourceforge.net/something/Something-1.2.3.dmg' + homepage 'https://sourceforge.net/projects/something/' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/sourceforge-incorrect-url-format.rb b/Library/Homebrew/cask/spec/support/Casks/sourceforge-incorrect-url-format.rb new file mode 100644 index 0000000000..27b5490b72 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/sourceforge-incorrect-url-format.rb @@ -0,0 +1,6 @@ +test_cask 'sourceforge-incorrect-url-format' do + version '1.2.3' + + url 'http://sourceforge.net/projects/something/files/Something-1.2.3.dmg/download' + homepage 'http://sourceforge.net/projects/something/' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/sourceforge-version-latest-correct-url-format.rb b/Library/Homebrew/cask/spec/support/Casks/sourceforge-version-latest-correct-url-format.rb new file mode 100644 index 0000000000..d9546c5e14 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/sourceforge-version-latest-correct-url-format.rb @@ -0,0 +1,6 @@ +test_cask 'sourceforge-version-latest-correct-url-format' do + version :latest + + url 'https://sourceforge.net/projects/something/files/latest/download' + homepage 'https://sourceforge.net/projects/something/' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/version-latest-string.rb b/Library/Homebrew/cask/spec/support/Casks/version-latest-string.rb new file mode 100644 index 0000000000..3b4723f45c --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/version-latest-string.rb @@ -0,0 +1,4 @@ +test_cask 'version-latest-string' do + version 'latest' + sha256 :no_check +end diff --git a/Library/Homebrew/cask/spec/support/Casks/version-latest-with-checksum.rb b/Library/Homebrew/cask/spec/support/Casks/version-latest-with-checksum.rb new file mode 100644 index 0000000000..8dbca1a690 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/version-latest-with-checksum.rb @@ -0,0 +1,4 @@ +test_cask 'version-latest-with-checksum' do + version :latest + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/with-binary.rb b/Library/Homebrew/cask/spec/support/Casks/with-binary.rb new file mode 100644 index 0000000000..a067871250 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/with-binary.rb @@ -0,0 +1,10 @@ +test_cask 'with-binary' do + version '1.2.3' + sha256 'd5b2dfbef7ea28c25f7a77cd7fa14d013d82b626db1d82e00e25822464ba19e2' + + url FileHelper.local_binary_url('AppWithBinary.zip') + homepage 'http://example.com/with-binary' + + app 'App.app' + binary 'binary' +end diff --git a/Library/Homebrew/cask/spec/support/Casks/with-embedded-binary.rb b/Library/Homebrew/cask/spec/support/Casks/with-embedded-binary.rb new file mode 100644 index 0000000000..796f28c78b --- /dev/null +++ b/Library/Homebrew/cask/spec/support/Casks/with-embedded-binary.rb @@ -0,0 +1,10 @@ +test_cask 'with-embedded-binary' do + version '1.2.3' + sha256 'fe052d3e77d92676775fd916ddb8942e72a565b844ea7f6d055474c99bb4e47b' + + url FileHelper.local_binary_url('AppWithEmbeddedBinary.zip') + homepage 'http://example.com/with-binary' + + app 'App.app' + binary "#{appdir}/App.app/Contents/MacOS/App/binary" +end diff --git a/Library/Homebrew/cask/spec/support/audit_matchers.rb b/Library/Homebrew/cask/spec/support/audit_matchers.rb new file mode 100644 index 0000000000..fc1e0e876c --- /dev/null +++ b/Library/Homebrew/cask/spec/support/audit_matchers.rb @@ -0,0 +1,39 @@ +module AuditMatchers + extend RSpec::Matchers::DSL + + matcher :pass do + match do |audit| + !audit.errors? && !audit.warnings? + end + end + + matcher :fail do + match(&:errors?) + end + + matcher :warn do + match do |audit| + audit.warnings? && !audit.errors? + end + end + + matcher :fail_with do |error_msg| + match do |audit| + include_msg?(audit.errors, error_msg) + end + end + + matcher :warn_with do |warning_msg| + match do |audit| + include_msg?(audit.warnings, warning_msg) + end + end + + def include_msg?(messages, msg) + if msg.is_a?(Regexp) + Array(messages).any? { |m| m =~ msg } + else + Array(messages).include?(msg) + end + end +end diff --git a/Library/Homebrew/cask/spec/support/binaries/AppWithBinary.zip b/Library/Homebrew/cask/spec/support/binaries/AppWithBinary.zip new file mode 100644 index 0000000000..4a5b318ba3 Binary files /dev/null and b/Library/Homebrew/cask/spec/support/binaries/AppWithBinary.zip differ diff --git a/Library/Homebrew/cask/spec/support/binaries/AppWithEmbeddedBinary.zip b/Library/Homebrew/cask/spec/support/binaries/AppWithEmbeddedBinary.zip new file mode 100644 index 0000000000..7c708038c5 Binary files /dev/null and b/Library/Homebrew/cask/spec/support/binaries/AppWithEmbeddedBinary.zip differ diff --git a/Library/Homebrew/cask/spec/support/env_helper.rb b/Library/Homebrew/cask/spec/support/env_helper.rb new file mode 100644 index 0000000000..0a302ef45f --- /dev/null +++ b/Library/Homebrew/cask/spec/support/env_helper.rb @@ -0,0 +1,16 @@ +module EnvHelper + class << self + def with_env_var(key, val) + was_defined = ENV.key? "key" + old_value = ENV["key"] + ENV[key] = val + yield + ensure + if was_defined + ENV[key] = old_value + else + ENV.delete(key) + end + end + end +end diff --git a/Library/Homebrew/cask/spec/support/expectations_hash_helper.rb b/Library/Homebrew/cask/spec/support/expectations_hash_helper.rb new file mode 100644 index 0000000000..8d386767da --- /dev/null +++ b/Library/Homebrew/cask/spec/support/expectations_hash_helper.rb @@ -0,0 +1,10 @@ +module ExpectationsHashHelper + shared_examples "expectations hash" do |input_name, expectations| + expectations.each do |input_value, expected_output| + context "when #{input_name} is #{input_value.inspect}" do + let(input_name.to_sym) { input_value } + it { should == expected_output } + end + end + end +end diff --git a/Library/Homebrew/cask/spec/support/file_helper.rb b/Library/Homebrew/cask/spec/support/file_helper.rb new file mode 100644 index 0000000000..8eb78f759f --- /dev/null +++ b/Library/Homebrew/cask/spec/support/file_helper.rb @@ -0,0 +1,16 @@ +module FileHelper + class << self + def local_binary_path(name) + File.expand_path(File.join(File.dirname(__FILE__), "binaries", name)) + end + + def local_binary_url(name) + "file://" + local_binary_path(name) + end + + def valid_alias?(candidate) + return false unless candidate.symlink? + candidate.readlink.exist? + end + end +end diff --git a/Library/Homebrew/cask/spec/support/install_helper.rb b/Library/Homebrew/cask/spec/support/install_helper.rb new file mode 100644 index 0000000000..c8023c66b3 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/install_helper.rb @@ -0,0 +1,10 @@ +module InstallHelper + class << self + def install_without_artifacts(cask) + Hbc::Installer.new(cask).tap do |i| + i.download + i.extract_primary_container + end + end + end +end diff --git a/Library/Homebrew/cask/spec/support/kernel_at_exit_hacks.rb b/Library/Homebrew/cask/spec/support/kernel_at_exit_hacks.rb new file mode 100644 index 0000000000..b5c84869b3 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/kernel_at_exit_hacks.rb @@ -0,0 +1,13 @@ +module Kernel + alias real_at_exit at_exit + + def at_exit(&block) + real_at_exit(&block) unless ENV["DISABLE_AT_EXIT"] + end + + def with_disabled_at_exit + ENV["DISABLE_AT_EXIT"] = "1" + yield + ENV.delete("DISABLE_AT_EXIT") + end +end diff --git a/Library/Homebrew/cask/spec/support/sha256_helper.rb b/Library/Homebrew/cask/spec/support/sha256_helper.rb new file mode 100644 index 0000000000..14f2a2519f --- /dev/null +++ b/Library/Homebrew/cask/spec/support/sha256_helper.rb @@ -0,0 +1,8 @@ +require"digest" + +module Sha256Helper + def random_sha256 + seed = "--#{rand(10_000)}--#{Time.now}--" + Digest::SHA2.hexdigest(seed) + end +end diff --git a/Library/Homebrew/cask/spec/support/shutup_helper.rb b/Library/Homebrew/cask/spec/support/shutup_helper.rb new file mode 100644 index 0000000000..98dde01a65 --- /dev/null +++ b/Library/Homebrew/cask/spec/support/shutup_helper.rb @@ -0,0 +1,18 @@ +module ShutupHelper + def shutup + if ENV.key?("VERBOSE_TESTS") + yield + else + begin + tmperr = $stderr.clone + tmpout = $stdout.clone + $stderr.reopen "/dev/null", "w" + $stdout.reopen "/dev/null", "w" + yield + ensure + $stderr.reopen tmperr + $stdout.reopen tmpout + end + end + end +end diff --git a/Library/Homebrew/cask/test/Casks/compliance_test.rb b/Library/Homebrew/cask/test/Casks/compliance_test.rb new file mode 100644 index 0000000000..fff3e07985 --- /dev/null +++ b/Library/Homebrew/cask/test/Casks/compliance_test.rb @@ -0,0 +1,14 @@ +require 'test_helper' + +describe "Casks" do + Hbc.all.reject {|c| c.is_a?(Hbc::TestCask) }.each do |cask| + describe "#{cask}" do + it "passes audit" do + audit = Hbc::Audit.new(cask) + audit.run! + audit.errors.must_equal [], "[#{cask}] Cask audit must be error free" + audit.warnings.must_equal [], "[#{cask}] Cask audit must be warning free" + end + end + end +end diff --git a/Library/Homebrew/cask/test/README.md b/Library/Homebrew/cask/test/README.md new file mode 100644 index 0000000000..4b73af8c4c --- /dev/null +++ b/Library/Homebrew/cask/test/README.md @@ -0,0 +1,3 @@ +# Naming + +Only files matching `*_test.rb` will be executed as tests. diff --git a/Library/Homebrew/cask/test/cask/accessibility_test.rb b/Library/Homebrew/cask/test/cask/accessibility_test.rb new file mode 100644 index 0000000000..ee192b4cb1 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/accessibility_test.rb @@ -0,0 +1,60 @@ +require "test_helper" + +# TODO: this test should be named after the corresponding class, once +# that class is abstracted from installer.rb. +describe "Accessibility Access" do + before do + cask = Hbc.load("with-accessibility-access") + with_fake_command = { command: Hbc::FakeSystemCommand } + @installer = Hbc::Installer.new(cask, with_fake_command) + end + + describe "install" do + it "can enable accessibility access" do + MacOS.stubs(version: MacOS::Version.new("10.9")) + + @installer.stubs(bundle_identifier: "com.example.BasicCask") + + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/sudo", "-E", "--", "/usr/bin/sqlite3", Hbc.tcc_db, "INSERT OR REPLACE INTO access VALUES('kTCCServiceAccessibility','com.example.BasicCask',0,1,1,NULL);"] + ) + shutup do + @installer.enable_accessibility_access + end + end + + it "can enable accessibility access in OS X releases prior to Mavericks" do + MacOS.stubs(version: MacOS::Version.new("10.8")) + + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/sudo", "-E", "--", "/usr/bin/touch", Hbc.pre_mavericks_accessibility_dotfile] + ) + shutup do + @installer.enable_accessibility_access + end + end + end + + describe "uninstall" do + it "can disable accessibility access" do + MacOS.stubs(version: MacOS::Version.new("10.9")) + + @installer.stubs(bundle_identifier: "com.example.BasicCask") + + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/sudo", "-E", "--", "/usr/bin/sqlite3", Hbc.tcc_db, "DELETE FROM access WHERE client='com.example.BasicCask';"] + ) + shutup do + @installer.disable_accessibility_access + end + end + it "warns about disabling accessibility access on old OS X releases" do + MacOS.stubs(version: MacOS::Version.new("10.8")) + + @installer.stubs(bundle_identifier: "com.example.BasicCask") + + capture_io { @installer.disable_accessibility_access }[1] + .must_match("Warning: Accessibility access was enabled for with-accessibility-access, but it is not safe to disable") + end + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/alt_target_test.rb b/Library/Homebrew/cask/test/cask/artifact/alt_target_test.rb new file mode 100644 index 0000000000..1d1dba254c --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/alt_target_test.rb @@ -0,0 +1,85 @@ +require "test_helper" + +describe Hbc::Artifact::App do + let(:local_alt_caffeine) { + Hbc.load("with-alt-target").tap do |cask| + TestHelper.install_without_artifacts(cask) + end + } + + describe "activate to alternate target" do + it "installs the given apps using the proper target directory" do + cask = local_alt_caffeine + + shutup do + Hbc::Artifact::App.new(cask).install_phase + end + + File.ftype(Hbc.appdir.join("AnotherName.app")).must_equal "directory" + File.exist?(cask.staged_path.join("AnotherName.app")).must_equal false + end + + it "works with an application in a subdir" do + subdir_cask = Hbc::Cask.new("subdir") do + url TestHelper.local_binary_url("caffeine.zip") + homepage "http://example.com/local-caffeine" + version "1.2.3" + sha256 "9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853" + app "subdir/Caffeine.app", target: "AnotherName.app" + end + + begin + TestHelper.install_without_artifacts(subdir_cask) + + appsubdir = subdir_cask.staged_path.join("subdir").tap(&:mkpath) + FileUtils.mv(subdir_cask.staged_path.join("Caffeine.app"), appsubdir) + + shutup do + Hbc::Artifact::App.new(subdir_cask).install_phase + end + + File.ftype(Hbc.appdir.join("AnotherName.app")).must_equal "directory" + File.exist?(appsubdir.join("AnotherName.app")).must_equal false + ensure + if defined?(subdir_cask) + shutup do + Hbc::Installer.new(subdir_cask).uninstall + end + end + end + end + + it "only uses apps when they are specified" do + cask = local_alt_caffeine + + staged_app_path = cask.staged_path.join("Caffeine.app") + staged_app_copy = staged_app_path.sub("Caffeine.app", "CaffeineAgain.app") + FileUtils.cp_r staged_app_path, staged_app_copy + + shutup do + Hbc::Artifact::App.new(cask).install_phase + end + + File.ftype(Hbc.appdir.join("AnotherName.app")).must_equal "directory" + File.exist?(staged_app_path).must_equal false + + File.exist?(Hbc.appdir.join("AnotherNameAgain.app")).must_equal false + File.exist?(cask.staged_path.join("CaffeineAgain.app")).must_equal true + end + + it "avoids clobbering an existing app by moving over it" do + cask = local_alt_caffeine + + existing_app_path = Hbc.appdir.join("AnotherName.app") + existing_app_path.mkpath + + TestHelper.must_output(self, lambda { + Hbc::Artifact::App.new(cask).install_phase + }, "==> It seems there is already an App at '#{existing_app_path}'; not moving.") + + source_path = cask.staged_path.join("Caffeine.app") + + File.identical?(source_path, existing_app_path).must_equal false + end + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/app_test.rb b/Library/Homebrew/cask/test/cask/artifact/app_test.rb new file mode 100644 index 0000000000..c5aa783650 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/app_test.rb @@ -0,0 +1,301 @@ +require "test_helper" + +describe Hbc::Artifact::App do + let(:local_caffeine) { + Hbc.load("local-caffeine").tap do |cask| + TestHelper.install_without_artifacts(cask) + end + } + + describe "install_phase" do + it "installs the given apps using the proper target directory" do + cask = local_caffeine + + shutup do + Hbc::Artifact::App.new(cask).install_phase + end + + File.ftype(Hbc.appdir.join("Caffeine.app")).must_equal "directory" + File.exist?(cask.staged_path.join("Caffeine.app")).must_equal false + end + + it "works with an application in a subdir" do + subdir_cask = Hbc::Cask.new("subdir") do + url TestHelper.local_binary_url("caffeine.zip") + homepage "http://example.com/local-caffeine" + version "1.2.3" + sha256 "9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853" + app "subdir/Caffeine.app" + end + + begin + TestHelper.install_without_artifacts(subdir_cask) + + appsubdir = subdir_cask.staged_path.join("subdir").tap(&:mkpath) + FileUtils.mv(subdir_cask.staged_path.join("Caffeine.app"), appsubdir) + + shutup do + Hbc::Artifact::App.new(subdir_cask).install_phase + end + + File.ftype(Hbc.appdir.join("Caffeine.app")).must_equal "directory" + File.exist?(appsubdir.join("Caffeine.app")).must_equal false + ensure + if defined?(subdir_cask) + shutup do + Hbc::Installer.new(subdir_cask).uninstall + end + end + end + end + + it "only uses apps when they are specified" do + cask = local_caffeine + + staged_app_path = cask.staged_path.join("Caffeine.app") + staged_app_copy = staged_app_path.sub("Caffeine.app", "CaffeineAgain.app") + FileUtils.cp_r staged_app_path, staged_app_copy + + shutup do + Hbc::Artifact::App.new(cask).install_phase + end + + File.ftype(Hbc.appdir.join("Caffeine.app")).must_equal "directory" + File.exist?(staged_app_path).must_equal false + + File.exist?(Hbc.appdir.join("CaffeineAgain.app")).must_equal false + File.exist?(cask.staged_path.join("CaffeineAgain.app")).must_equal true + end + + describe "when the target already exists" do + let(:target_path) { + target_path = Hbc.appdir.join("Caffeine.app") + target_path.mkpath + target_path + } + + it "avoids clobbering an existing app" do + cask = local_caffeine + + TestHelper.must_output(self, lambda { + Hbc::Artifact::App.new(cask).install_phase + }, "==> It seems there is already an App at '#{target_path}'; not moving.") + + source_path = cask.staged_path.join("Caffeine.app") + + File.identical?(source_path, target_path).must_equal false + + contents_path = target_path.join("Contents/Info.plist") + File.exist?(contents_path).must_equal false + end + + describe "given the force option" do + let(:install_phase) { + lambda do |given_options = {}| + options = { force: true }.merge(given_options) + Hbc::Artifact::App.new(local_caffeine, options).install_phase + end + } + + let(:chmod_cmd) { + ["/bin/chmod", "-R", "--", "u+rwx", target_path] + } + + let(:chmod_n_cmd) { + ["/bin/chmod", "-R", "-N", target_path] + } + + let(:chflags_cmd) { + ["/usr/bin/chflags", "-R", "--", "000", target_path] + } + + before do + Hbc::Utils.stubs(current_user: "fake_user") + end + + describe "target is both writable and user-owned" do + it "overwrites the existing app" do + cask = local_caffeine + + expected = [ + "==> It seems there is already an App at '#{target_path}'; overwriting.", + "==> Removing App: '#{target_path}'", + "==> Moving App 'Caffeine.app' to '#{target_path}'", + ] + TestHelper.must_output(self, install_phase, + expected.join("\n")) + + source_path = cask.staged_path.join("Caffeine.app") + + File.exist?(source_path).must_equal false + File.ftype(target_path).must_equal "directory" + + contents_path = target_path.join("Contents/Info.plist") + File.exist?(contents_path).must_equal true + end + end + + describe "target is user-owned but contains read-only files" do + before do + system "/usr/bin/touch", "--", "#{target_path}/foo" + system "/bin/chmod", "--", "0555", target_path + end + + it "tries to make the target world-writable" do + Hbc::FakeSystemCommand.expect_and_pass_through(chflags_cmd) + Hbc::FakeSystemCommand.expect_and_pass_through(chmod_cmd) + Hbc::FakeSystemCommand.expect_and_pass_through(chmod_n_cmd) + + shutup do + install_phase.call(command: Hbc::FakeSystemCommand) + end + end + + it "overwrites the existing app" do + cask = local_caffeine + + expected = [ + "==> It seems there is already an App at '#{target_path}'; overwriting.", + "==> Removing App: '#{target_path}'", + "==> Moving App 'Caffeine.app' to '#{target_path}'", + ] + TestHelper.must_output(self, install_phase, + expected.join("\n")) + + source_path = cask.staged_path.join("Caffeine.app") + + File.exist?(source_path).must_equal false + File.ftype(target_path).must_equal "directory" + + contents_path = target_path.join("Contents/Info.plist") + File.exist?(contents_path).must_equal true + end + + after do + system "/bin/chmod", "--", "0755", target_path + end + end + end + end + + describe "when the target is a broken symlink" do + let(:target_path) { + Hbc.appdir.join("Caffeine.app") + } + + let(:deleted_path) { + local_caffeine.staged_path.join( + "Deleted.app" + ) + } + + before do + deleted_path.mkdir + File.symlink(deleted_path, target_path) + deleted_path.rmdir + end + + it "leaves the target alone" do + cask = local_caffeine + TestHelper.must_output(self, lambda { + Hbc::Artifact::App.new(cask).install_phase + }, "==> It seems there is already an App at '#{target_path}'; not moving.") + + File.symlink?(target_path).must_equal true + end + + describe "given the force option" do + let(:install_phase) { + lambda do + Hbc::Artifact::App.new( + local_caffeine, force: true + ).install_phase + end + } + + it "overwrites the existing app" do + cask = local_caffeine + + expected = [ + "==> It seems there is already an App at '#{target_path}'; overwriting.", + "==> Removing App: '#{target_path}'", + "==> Moving App 'Caffeine.app' to '#{target_path}'", + ] + TestHelper.must_output(self, install_phase, + expected.join("\n")) + + source_path = cask.staged_path.join("Caffeine.app") + + File.exist?(source_path).must_equal false + File.ftype(target_path).must_equal "directory" + + contents_path = target_path.join("Contents/Info.plist") + File.exist?(contents_path).must_equal true + end + end + end + + it "gives a warning if the source doesn't exist" do + cask = local_caffeine + staged_app_path = cask.staged_path.join("Caffeine.app") + staged_app_path.rmtree + + installation = -> { Hbc::Artifact::App.new(cask).install_phase } + message = "It seems the App source is not there: '#{staged_app_path}'" + + error = installation.must_raise(Hbc::CaskError) + error.message.must_equal message + end + end + + describe "uninstall_phase" do + it "deletes managed apps" do + cask = local_caffeine + + shutup do + Hbc::Artifact::App.new(cask).install_phase + Hbc::Artifact::App.new(cask).uninstall_phase + end + + app_path = Hbc.appdir.join("Caffeine.app") + File.exist?(app_path).must_equal false + end + end + + describe "summary" do + it "returns the correct english_description" do + cask = local_caffeine + + description = Hbc::Artifact::App.new(cask).summary[:english_description] + + description.must_equal "Apps" + end + + describe "app is correctly installed" do + it "returns the path to the app" do + cask = local_caffeine + + shutup do + Hbc::Artifact::App.new(cask).install_phase + end + + contents = Hbc::Artifact::App.new(cask).summary[:contents] + app_path = Hbc.appdir.join("Caffeine.app") + + contents.must_equal ["#{app_path} (#{app_path.abv})"] + end + end + + describe "app is missing" do + it "returns a warning and the supposed path to the app" do + cask = local_caffeine + + contents = Hbc::Artifact::App.new(cask).summary[:contents] + app_path = Hbc.appdir.join("Caffeine.app") + + contents.size.must_equal 1 + contents[0].must_match(%r{.*Missing App.*: #{app_path}}) + end + end + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/generic_artifact_test.rb b/Library/Homebrew/cask/test/cask/artifact/generic_artifact_test.rb new file mode 100644 index 0000000000..d4a1733bab --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/generic_artifact_test.rb @@ -0,0 +1,44 @@ +require "test_helper" + +describe Hbc::Artifact::Artifact do + let(:cask) { + Hbc.load("with-generic-artifact").tap do |cask| + TestHelper.install_without_artifacts(cask) + end + } + let(:expected_path) { + Hbc.appdir.join("Caffeine.app") + } + + it "fails to install with no target" do + no_target = Hbc.load("with-generic-artifact-no-target") + TestHelper.install_without_artifacts(no_target) + + lambda { + shutup do + Hbc::Artifact::Artifact.new(no_target).install_phase + end + }.must_raise(Hbc::CaskInvalidError) + end + + it "moves the artifact to the proper directory" do + shutup do + Hbc::Artifact::Artifact.new(cask).install_phase + end + + File.ftype(Hbc.appdir.join("Caffeine.app")).must_equal "directory" + File.exist?(cask.staged_path.join("Caffeine.app")).must_equal false + end + + it "avoids clobbering an existing artifact" do + FileUtils.touch expected_path + + shutup do + Hbc::Artifact::Artifact.new(cask).install_phase + end + + source_path = cask.staged_path.join("Caffeine.app") + + File.identical?(source_path, expected_path).must_equal false + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/nested_container_test.rb b/Library/Homebrew/cask/test/cask/artifact/nested_container_test.rb new file mode 100644 index 0000000000..b771ba3452 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/nested_container_test.rb @@ -0,0 +1,17 @@ +require "test_helper" + +describe Hbc::Artifact::NestedContainer do + describe "install" do + it "extracts the specified paths as containers" do + cask = Hbc.load("nested-app").tap do |c| + TestHelper.install_without_artifacts(c) + end + + shutup do + Hbc::Artifact::NestedContainer.new(cask).install_phase + end + + cask.staged_path.join("MyNestedApp.app").must_be :directory? + end + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/pkg_test.rb b/Library/Homebrew/cask/test/cask/artifact/pkg_test.rb new file mode 100644 index 0000000000..e87db7a7ab --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/pkg_test.rb @@ -0,0 +1,33 @@ +require "test_helper" + +describe Hbc::Artifact::Pkg do + before do + @cask = Hbc.load("with-installable") + shutup do + TestHelper.install_without_artifacts(@cask) + end + end + + describe "install_phase" do + it "runs the system installer on the specified pkgs" do + pkg = Hbc::Artifact::Pkg.new(@cask, + command: Hbc::FakeSystemCommand) + + Hbc::FakeSystemCommand.expects_command(["/usr/bin/sudo", "-E", "--", "/usr/sbin/installer", "-pkg", @cask.staged_path.join("MyFancyPkg", "Fancy.pkg"), "-target", "/"]) + + shutup do + pkg.install_phase + end + end + end + + describe "uninstall_phase" do + it "does nothing, because the uninstall_phase method is a no-op" do + pkg = Hbc::Artifact::Pkg.new(@cask, + command: Hbc::FakeSystemCommand) + shutup do + pkg.uninstall_phase + end + end + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/postflight_block_test.rb b/Library/Homebrew/cask/test/cask/artifact/postflight_block_test.rb new file mode 100644 index 0000000000..47dcdd905b --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/postflight_block_test.rb @@ -0,0 +1,41 @@ +require "test_helper" + +describe Hbc::Artifact::PostflightBlock do + describe "install_phase" do + it "calls the specified block after installing, passing a Cask mini-dsl" do + called = false + yielded_arg = nil + + cask = Hbc::Cask.new("with-postflight") do + postflight do |c| + called = true + yielded_arg = c + end + end + + Hbc::Artifact::PostflightBlock.new(cask).install_phase + + called.must_equal true + yielded_arg.must_be_kind_of Hbc::DSL::Postflight + end + end + + describe "uninstall_phase" do + it "calls the specified block after uninstalling, passing a Cask mini-dsl" do + called = false + yielded_arg = nil + + cask = Hbc::Cask.new("with-uninstall-postflight") do + uninstall_postflight do |c| + called = true + yielded_arg = c + end + end + + Hbc::Artifact::PostflightBlock.new(cask).uninstall_phase + + called.must_equal true + yielded_arg.must_be_kind_of Hbc::DSL::UninstallPostflight + end + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/preflight_block_test.rb b/Library/Homebrew/cask/test/cask/artifact/preflight_block_test.rb new file mode 100644 index 0000000000..440b1db3a4 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/preflight_block_test.rb @@ -0,0 +1,41 @@ +require "test_helper" + +describe Hbc::Artifact::PreflightBlock do + describe "install_phase" do + it "calls the specified block before installing, passing a Cask mini-dsl" do + called = false + yielded_arg = nil + + cask = Hbc::Cask.new("with-preflight") do + preflight do |c| + called = true + yielded_arg = c + end + end + + Hbc::Artifact::PreflightBlock.new(cask).install_phase + + called.must_equal true + yielded_arg.must_be_kind_of Hbc::DSL::Preflight + end + end + + describe "uninstall_phase" do + it "calls the specified block before uninstalling, passing a Cask mini-dsl" do + called = false + yielded_arg = nil + + cask = Hbc::Cask.new("with-uninstall-preflight") do + uninstall_preflight do |c| + called = true + yielded_arg = c + end + end + + Hbc::Artifact::PreflightBlock.new(cask).uninstall_phase + + called.must_equal true + yielded_arg.must_be_kind_of Hbc::DSL::UninstallPreflight + end + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/suite_test.rb b/Library/Homebrew/cask/test/cask/artifact/suite_test.rb new file mode 100644 index 0000000000..5f72c45659 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/suite_test.rb @@ -0,0 +1,41 @@ +require "test_helper" + +describe Hbc::Artifact::Suite do + let(:cask) { + Hbc.load("with-suite").tap do |cask| + TestHelper.install_without_artifacts(cask) + end + } + let(:expected_path) { + Hbc.appdir.join("caffeine_suite") + } + let(:source_path) { cask.staged_path.join("caffeine_suite") } + + it "moves the suite to the proper directory" do + shutup do + Hbc::Artifact::Suite.new(cask).install_phase + end + + expected_path.must_be :directory? + TestHelper.valid_alias?(expected_path).must_equal false + File.exist?(source_path).must_equal false + end + + it "creates a suite containing the expected app" do + shutup do + Hbc::Artifact::Suite.new(cask).install_phase + end + + expected_path.join("Caffeine.app").must_be :exist? + end + + it "avoids clobbering an existing suite by moving over it" do + FileUtils.touch expected_path + + shutup do + Hbc::Artifact::Suite.new(cask).install_phase + end + + File.identical?(source_path, expected_path).must_equal false + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/two_apps_correct_test.rb b/Library/Homebrew/cask/test/cask/artifact/two_apps_correct_test.rb new file mode 100644 index 0000000000..3432597e05 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/two_apps_correct_test.rb @@ -0,0 +1,97 @@ +require "test_helper" + +describe Hbc::Artifact::App do + let(:local_two_apps_caffeine) { + Hbc.load("with-two-apps-correct").tap do |cask| + TestHelper.install_without_artifacts(cask) + end + } + + let(:local_two_apps_subdir) { + Hbc.load("with-two-apps-subdir").tap do |cask| + TestHelper.install_without_artifacts(cask) + end + } + + describe "multiple apps" do + it "installs both apps using the proper target directory" do + cask = local_two_apps_caffeine + + shutup do + Hbc::Artifact::App.new(cask).install_phase + end + + File.ftype(Hbc.appdir.join("Caffeine.app")).must_equal "directory" + File.exist?(cask.staged_path.join("Caffeine.app")).must_equal false + + File.ftype(Hbc.appdir.join("Caffeine-2.app")).must_equal "directory" + File.exist?(cask.staged_path.join("Caffeine-2.app")).must_equal false + end + + it "works with an application in a subdir" do + cask = local_two_apps_subdir + TestHelper.install_without_artifacts(cask) + + shutup do + Hbc::Artifact::App.new(cask).install_phase + end + + File.ftype(Hbc.appdir.join("Caffeine.app")).must_equal "directory" + File.exist?(cask.staged_path.join("Caffeine.app")).must_equal false + + File.ftype(Hbc.appdir.join("Caffeine-2.app")).must_equal "directory" + File.exist?(cask.staged_path.join("Caffeine-2.app")).must_equal false + end + + it "only uses apps when they are specified" do + cask = local_two_apps_caffeine + + app_path = cask.staged_path.join("Caffeine.app") + FileUtils.cp_r app_path, app_path.sub("Caffeine.app", "CaffeineAgain.app") + + shutup do + Hbc::Artifact::App.new(cask).install_phase + end + + File.ftype(Hbc.appdir.join("Caffeine.app")).must_equal "directory" + File.exist?(cask.staged_path.join("Caffeine.app")).must_equal false + + File.exist?(Hbc.appdir.join("CaffeineAgain.app")).must_equal false + File.exist?(cask.staged_path.join("CaffeineAgain.app")).must_equal true + end + + it "avoids clobbering an existing app (app 1)" do + cask = local_two_apps_caffeine + + Hbc.appdir.join("Caffeine.app").mkpath + + TestHelper.must_output(self, lambda { + Hbc::Artifact::App.new(cask).install_phase + }, <<-MESSAGE.undent.chomp) + ==> Moving App 'Caffeine-2.app' to '#{Hbc.appdir.join('Caffeine-2.app')}' + ==> It seems there is already an App at '#{Hbc.appdir.join('Caffeine.app')}'; not moving. + MESSAGE + + source_path = cask.staged_path.join("Caffeine.app") + + File.identical?(source_path, Hbc.appdir.join("Caffeine.app")).must_equal false + end + + it "avoids clobbering an existing app (app 2)" do + cask = local_two_apps_caffeine + + Hbc.appdir.join("Caffeine-2.app").mkpath + + TestHelper.must_output(self, lambda { + Hbc::Artifact::App.new(cask).install_phase + }, <<-MESSAGE.undent.chomp) + ==> It seems there is already an App at '#{Hbc.appdir.join('Caffeine-2.app')}'; not moving. + ==> Moving App 'Caffeine.app' to '#{Hbc.appdir.join('Caffeine.app')}' + MESSAGE + + source_path = cask.staged_path.join("Caffeine-2.app") + + File.identical?(source_path, Hbc.appdir.join("Caffeine-2.app")).must_equal false + end + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/two_apps_incorrect_test.rb b/Library/Homebrew/cask/test/cask/artifact/two_apps_incorrect_test.rb new file mode 100644 index 0000000000..a89a94fbab --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/two_apps_incorrect_test.rb @@ -0,0 +1,14 @@ +require "test_helper" + +describe Hbc::Artifact::App do + it "must raise" do + exception_raised = begin + Hbc.load("two-apps-incorrect") + false + rescue + true + end + # TODO: later give the user a nice exception for this case and check for it here + assert exception_raised + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/uninstall_test.rb b/Library/Homebrew/cask/test/cask/artifact/uninstall_test.rb new file mode 100644 index 0000000000..4d6c5df1b5 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/uninstall_test.rb @@ -0,0 +1,334 @@ +require "test_helper" + +describe Hbc::Artifact::Uninstall do + let(:cask) { Hbc.load("with-installable") } + + let(:uninstall_artifact) { + Hbc::Artifact::Uninstall.new(cask, command: Hbc::FakeSystemCommand) + } + + before do + shutup do + TestHelper.install_without_artifacts(cask) + end + end + + describe "install_phase" do + it "does nothing, because the install_phase method is a no-op" do + shutup do + uninstall_artifact.install_phase + end + end + end + + describe "zap_phase" do + it "does nothing, because the zap_phase method is a no-op" do + shutup do + uninstall_artifact.zap_phase + end + end + end + + describe "uninstall_phase" do + subject do + shutup do + uninstall_artifact.uninstall_phase + end + end + + describe "when using launchctl" do + let(:cask) { Hbc.load("with-uninstall-launchctl") } + let(:launchctl_list_cmd) { %w[/bin/launchctl list my.fancy.package.service] } + let(:launchctl_remove_cmd) { %w[/bin/launchctl remove my.fancy.package.service] } + let(:unknown_response) { "launchctl list returned unknown response\n" } + let(:service_info) { + <<-PLIST.undent + { + "LimitLoadToSessionType" = "Aqua"; + "Label" = "my.fancy.package.service"; + "TimeOut" = 30; + "OnDemand" = true; + "LastExitStatus" = 0; + "ProgramArguments" = ( + "argument"; + ); + }; + PLIST + } + + describe "when launchctl job is owned by user" do + it "can uninstall" do + Hbc::FakeSystemCommand.stubs_command( + launchctl_list_cmd, + service_info + ) + + Hbc::FakeSystemCommand.stubs_command( + sudo(launchctl_list_cmd), + unknown_response + ) + + Hbc::FakeSystemCommand.expects_command(launchctl_remove_cmd) + + subject + end + end + + describe "when launchctl job is owned by system" do + it "can uninstall" do + Hbc::FakeSystemCommand.stubs_command( + launchctl_list_cmd, + unknown_response + ) + + Hbc::FakeSystemCommand.stubs_command( + sudo(launchctl_list_cmd), + service_info + ) + + Hbc::FakeSystemCommand.expects_command(sudo(launchctl_remove_cmd)) + + subject + end + end + end + + describe "when using pkgutil" do + let(:cask) { Hbc.load("with-uninstall-pkgutil") } + let(:main_pkg_id) { "my.fancy.package.main" } + let(:agent_pkg_id) { "my.fancy.package.agent" } + let(:main_files) { + %w[ + fancy/bin/fancy.exe + fancy/var/fancy.data + ] + } + let(:main_dirs) { + %w[ + fancy + fancy/bin + fancy/var + ] + } + let(:agent_files) { + %w[ + fancy/agent/fancy-agent.exe + fancy/agent/fancy-agent.pid + fancy/agent/fancy-agent.log + ] + } + let(:agent_dirs) { + %w[ + fancy + fancy/agent + ] + } + let(:pkg_info_plist) { + <<-PLIST.undent + + + + + install-location + tmp + volume + / + + + PLIST + } + + it "can uninstall" do + Hbc::FakeSystemCommand.stubs_command( + %w[/usr/sbin/pkgutil --pkgs=my.fancy.package.*], + "#{main_pkg_id}\n#{agent_pkg_id}" + ) + + [ + [main_pkg_id, main_files, main_dirs], + [agent_pkg_id, agent_files, agent_dirs], + ].each do |pkg_id, pkg_files, pkg_dirs| + Hbc::FakeSystemCommand.stubs_command( + %W[/usr/sbin/pkgutil --only-files --files #{pkg_id}], + pkg_files.join("\n") + ) + + Hbc::FakeSystemCommand.stubs_command( + %W[/usr/sbin/pkgutil --only-dirs --files #{pkg_id}], + pkg_dirs.join("\n") + ) + + Hbc::FakeSystemCommand.stubs_command( + %W[/usr/sbin/pkgutil --files #{pkg_id}], + (pkg_files + pkg_dirs).join("\n") + ) + + Hbc::FakeSystemCommand.stubs_command( + %W[/usr/sbin/pkgutil --pkg-info-plist #{pkg_id}], + pkg_info_plist + ) + + Hbc::FakeSystemCommand.expects_command(sudo(%W[/usr/sbin/pkgutil --forget #{pkg_id}])) + + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rm -f --] + pkg_files.map { |path| Pathname("/tmp/#{path}") }) + ) + end + + subject + end + end + + describe "when using kext" do + let(:cask) { Hbc.load("with-uninstall-kext") } + let(:kext_id) { "my.fancy.package.kernelextension" } + + it "can uninstall" do + Hbc::FakeSystemCommand.stubs_command( + sudo(%W[/usr/sbin/kextstat -l -b #{kext_id}]), "loaded" + ) + + Hbc::FakeSystemCommand.expects_command( + sudo(%W[/sbin/kextunload -b #{kext_id}]) + ) + + subject + end + end + + describe "when using quit" do + let(:cask) { Hbc.load("with-uninstall-quit") } + let(:bundle_id) { "my.fancy.package.app" } + let(:count_processes_script) { + 'tell application "System Events" to count processes ' + + %Q{whose bundle identifier is "#{bundle_id}"} + } + let(:quit_application_script) { + %Q{tell application id "#{bundle_id}" to quit} + } + + it "can uninstall" do + Hbc::FakeSystemCommand.stubs_command( + sudo(%W[/usr/bin/osascript -e #{count_processes_script}]), "1" + ) + + Hbc::FakeSystemCommand.stubs_command( + sudo(%W[/usr/bin/osascript -e #{quit_application_script}]) + ) + + subject + end + end + + describe "when using signal" do + let(:cask) { Hbc.load("with-uninstall-signal") } + let(:bundle_id) { "my.fancy.package.app" } + let(:signals) { %w[TERM KILL] } + let(:unix_pids) { [12_345, 67_890] } + let(:get_unix_pids_script) { + 'tell application "System Events" to get the unix id of every process ' + + %Q{whose bundle identifier is "#{bundle_id}"} + } + + it "can uninstall" do + Hbc::FakeSystemCommand.stubs_command( + sudo(%W[/usr/bin/osascript -e #{get_unix_pids_script}]), unix_pids.join(", ") + ) + + signals.each do |signal| + Process.expects(:kill).with(signal, *unix_pids) + end + + subject + end + end + + describe "when using delete" do + let(:cask) { Hbc.load("with-uninstall-delete") } + + it "can uninstall" do + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rm -rf --], + Pathname.new("/permissible/absolute/path"), + Pathname.new("~/permissible/path/with/tilde").expand_path) + ) + + subject + end + end + + describe "when using trash" do + let(:cask) { Hbc.load("with-uninstall-trash") } + + it "can uninstall" do + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rm -rf --], + Pathname.new("/permissible/absolute/path"), + Pathname.new("~/permissible/path/with/tilde").expand_path) + ) + + subject + end + end + + describe "when using rmdir" do + let(:cask) { Hbc.load("with-uninstall-rmdir") } + let(:dir_pathname) { Pathname(TestHelper.local_binary_path("empty_directory")) } + + it "can uninstall" do + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rm -f --], dir_pathname.join(".DS_Store")) + ) + + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rmdir --], dir_pathname) + ) + + subject + end + end + + describe "when using script" do + let(:cask) { Hbc.load("with-uninstall-script") } + let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } + + it "can uninstall" do + Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) + + Hbc::FakeSystemCommand.expects_command( + sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please") + ) + + subject + end + end + + describe "when using early_script" do + let(:cask) { Hbc.load("with-uninstall-early-script") } + let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } + + it "can uninstall" do + Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) + + Hbc::FakeSystemCommand.expects_command( + sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please") + ) + + subject + end + end + + describe "when using login_item" do + let(:cask) { Hbc.load("with-uninstall-login-item") } + + it "can uninstall" do + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/osascript", "-e", 'tell application "System Events" to delete every login ' \ + 'item whose name is "Fancy"'] + ) + + subject + end + end + end +end diff --git a/Library/Homebrew/cask/test/cask/artifact/zap_test.rb b/Library/Homebrew/cask/test/cask/artifact/zap_test.rb new file mode 100644 index 0000000000..8ffa1c5e30 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/artifact/zap_test.rb @@ -0,0 +1,335 @@ +require "test_helper" + +# TODO: test that zap removes an alternate version of the same Cask +describe Hbc::Artifact::Zap do + let(:cask) { Hbc.load("with-installable") } + + let(:zap_artifact) { + Hbc::Artifact::Zap.new(cask, command: Hbc::FakeSystemCommand) + } + + before do + shutup do + TestHelper.install_without_artifacts(cask) + end + end + + describe "install_phase" do + it "does nothing, because the install_phase method is a no-op" do + shutup do + zap_artifact.install_phase + end + end + end + + describe "uninstall_phase" do + it "does nothing, because the uninstall_phase method is a no-op" do + shutup do + zap_artifact.uninstall_phase + end + end + end + + describe "zap_phase" do + subject do + shutup do + zap_artifact.zap_phase + end + end + + describe "when using launchctl" do + let(:cask) { Hbc.load("with-zap-launchctl") } + let(:launchctl_list_cmd) { %w[/bin/launchctl list my.fancy.package.service] } + let(:launchctl_remove_cmd) { %w[/bin/launchctl remove my.fancy.package.service] } + let(:unknown_response) { "launchctl list returned unknown response\n" } + let(:service_info) { + <<-PLIST.undent + { + "LimitLoadToSessionType" = "Aqua"; + "Label" = "my.fancy.package.service"; + "TimeOut" = 30; + "OnDemand" = true; + "LastExitStatus" = 0; + "ProgramArguments" = ( + "argument"; + ); + }; + PLIST + } + + describe "when launchctl job is owned by user" do + it "can zap" do + Hbc::FakeSystemCommand.stubs_command( + launchctl_list_cmd, + service_info + ) + + Hbc::FakeSystemCommand.stubs_command( + sudo(launchctl_list_cmd), + unknown_response + ) + + Hbc::FakeSystemCommand.expects_command(launchctl_remove_cmd) + + subject + end + end + + describe "when launchctl job is owned by system" do + it "can zap" do + Hbc::FakeSystemCommand.stubs_command( + launchctl_list_cmd, + unknown_response + ) + + Hbc::FakeSystemCommand.stubs_command( + sudo(launchctl_list_cmd), + service_info + ) + + Hbc::FakeSystemCommand.expects_command(sudo(launchctl_remove_cmd)) + + subject + end + end + end + + describe "when using pkgutil" do + let(:cask) { Hbc.load("with-zap-pkgutil") } + let(:main_pkg_id) { "my.fancy.package.main" } + let(:agent_pkg_id) { "my.fancy.package.agent" } + let(:main_files) { + %w[ + fancy/bin/fancy.exe + fancy/var/fancy.data + ] + } + let(:main_dirs) { + %w[ + fancy + fancy/bin + fancy/var + ] + } + let(:agent_files) { + %w[ + fancy/agent/fancy-agent.exe + fancy/agent/fancy-agent.pid + fancy/agent/fancy-agent.log + ] + } + let(:agent_dirs) { + %w[ + fancy + fancy/agent + ] + } + let(:pkg_info_plist) { + <<-PLIST.undent + + + + + install-location + tmp + volume + / + + + PLIST + } + + it "can zap" do + Hbc::FakeSystemCommand.stubs_command( + %w[/usr/sbin/pkgutil --pkgs=my.fancy.package.*], + "#{main_pkg_id}\n#{agent_pkg_id}" + ) + + [ + [main_pkg_id, main_files, main_dirs], + [agent_pkg_id, agent_files, agent_dirs], + ].each do |pkg_id, pkg_files, pkg_dirs| + Hbc::FakeSystemCommand.stubs_command( + %W[/usr/sbin/pkgutil --only-files --files #{pkg_id}], + pkg_files.join("\n") + ) + + Hbc::FakeSystemCommand.stubs_command( + %W[/usr/sbin/pkgutil --only-dirs --files #{pkg_id}], + pkg_dirs.join("\n") + ) + + Hbc::FakeSystemCommand.stubs_command( + %W[/usr/sbin/pkgutil --files #{pkg_id}], + (pkg_files + pkg_dirs).join("\n") + ) + + Hbc::FakeSystemCommand.stubs_command( + %W[/usr/sbin/pkgutil --pkg-info-plist #{pkg_id}], + pkg_info_plist + ) + + Hbc::FakeSystemCommand.expects_command(sudo(%W[/usr/sbin/pkgutil --forget #{pkg_id}])) + + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rm -f --] + pkg_files.map { |path| Pathname("/tmp/#{path}") }) + ) + end + + subject + end + end + + describe "when using kext" do + let(:cask) { Hbc.load("with-zap-kext") } + let(:kext_id) { "my.fancy.package.kernelextension" } + + it "can zap" do + Hbc::FakeSystemCommand.stubs_command( + sudo(%W[/usr/sbin/kextstat -l -b #{kext_id}]), "loaded" + ) + + Hbc::FakeSystemCommand.expects_command( + sudo(%W[/sbin/kextunload -b #{kext_id}]) + ) + + subject + end + end + + describe "when using quit" do + let(:cask) { Hbc.load("with-zap-quit") } + let(:bundle_id) { "my.fancy.package.app" } + let(:count_processes_script) { + 'tell application "System Events" to count processes ' + + %Q{whose bundle identifier is "#{bundle_id}"} + } + let(:quit_application_script) { + %Q{tell application id "#{bundle_id}" to quit} + } + + it "can zap" do + Hbc::FakeSystemCommand.stubs_command( + sudo(%W[/usr/bin/osascript -e #{count_processes_script}]), "1" + ) + + Hbc::FakeSystemCommand.stubs_command( + sudo(%W[/usr/bin/osascript -e #{quit_application_script}]) + ) + + subject + end + end + + describe "when using signal" do + let(:cask) { Hbc.load("with-zap-signal") } + let(:bundle_id) { "my.fancy.package.app" } + let(:signals) { %w[TERM KILL] } + let(:unix_pids) { [12_345, 67_890] } + let(:get_unix_pids_script) { + 'tell application "System Events" to get the unix id of every process ' + + %Q{whose bundle identifier is "#{bundle_id}"} + } + + it "can zap" do + Hbc::FakeSystemCommand.stubs_command( + sudo(%W[/usr/bin/osascript -e #{get_unix_pids_script}]), unix_pids.join(", ") + ) + + signals.each do |signal| + Process.expects(:kill).with(signal, *unix_pids) + end + + subject + end + end + + describe "when using delete" do + let(:cask) { Hbc.load("with-zap-delete") } + + it "can zap" do + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rm -rf --], + Pathname.new("/permissible/absolute/path"), + Pathname.new("~/permissible/path/with/tilde").expand_path) + ) + + subject + end + end + + describe "when using trash" do + let(:cask) { Hbc.load("with-zap-trash") } + + it "can zap" do + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rm -rf --], + Pathname.new("/permissible/absolute/path"), + Pathname.new("~/permissible/path/with/tilde").expand_path) + ) + + subject + end + end + + describe "when using rmdir" do + let(:cask) { Hbc.load("with-zap-rmdir") } + let(:dir_pathname) { Pathname(TestHelper.local_binary_path("empty_directory")) } + + it "can zap" do + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rm -f --], dir_pathname.join(".DS_Store")) + ) + + Hbc::FakeSystemCommand.expects_command( + sudo(%w[/bin/rmdir --], dir_pathname) + ) + + subject + end + end + + describe "when using script" do + let(:cask) { Hbc.load("with-zap-script") } + let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } + + it "can zap" do + Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) + + Hbc::FakeSystemCommand.expects_command( + sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please") + ) + + subject + end + end + + describe "when using early_script" do + let(:cask) { Hbc.load("with-zap-early-script") } + let(:script_pathname) { cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool") } + + it "can zap" do + Hbc::FakeSystemCommand.expects_command(%w[/bin/chmod -- +x] + [script_pathname]) + + Hbc::FakeSystemCommand.expects_command( + sudo(cask.staged_path.join("MyFancyPkg", "FancyUninstaller.tool"), "--please") + ) + + subject + end + end + + describe "when using login_item" do + let(:cask) { Hbc.load("with-zap-login-item") } + + it "can zap" do + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/osascript", "-e", 'tell application "System Events" to delete every login ' \ + 'item whose name is "Fancy"'] + ) + + subject + end + end + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/audit_test.rb b/Library/Homebrew/cask/test/cask/cli/audit_test.rb new file mode 100644 index 0000000000..b55c4ea303 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/audit_test.rb @@ -0,0 +1,63 @@ +require "test_helper" + +describe Hbc::CLI::Audit do + let(:auditor) { mock } + let(:cask) { mock } + + describe "selection of Casks to audit" do + it "audits all Casks if no tokens are given" do + Hbc.stubs(all: [cask, cask]) + auditor.expects(:audit).times(2) + + run_audit([], auditor) + end + + it "audits specified Casks if tokens are given" do + cask_token = "nice-app" + Hbc.expects(:load).with(cask_token).returns(cask) + auditor.expects(:audit).with(cask, audit_download: false, check_token_conflicts: false) + + run_audit([cask_token], auditor) + end + end + + describe "rules for downloading a Cask" do + it "does not download the Cask per default" do + Hbc.stubs(load: cask) + + auditor.expects(:audit).with(cask, audit_download: false, check_token_conflicts: false) + + run_audit(["casktoken"], auditor) + end + + it "download a Cask if --download flag is set" do + Hbc.stubs(load: cask) + + auditor.expects(:audit).with(cask, audit_download: true, check_token_conflicts: false) + + run_audit(["casktoken", "--download"], auditor) + end + end + + describe "rules for checking token conflicts" do + it "does not check for token conflicts per default" do + Hbc.stubs(load: cask) + + auditor.expects(:audit).with(cask, audit_download: false, check_token_conflicts: false) + + run_audit(["casktoken"], auditor) + end + + it "checks for token conflicts if --token-conflicts flag is set" do + Hbc.stubs(load: cask) + + auditor.expects(:audit).with(cask, audit_download: false, check_token_conflicts: true) + + run_audit(["casktoken", "--token-conflicts"], auditor) + end + end + + def run_audit(args, auditor) + Hbc::CLI::Audit.new(args, auditor).run + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/cat_test.rb b/Library/Homebrew/cask/test/cask/cli/cat_test.rb new file mode 100644 index 0000000000..f51fe4be09 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/cat_test.rb @@ -0,0 +1,59 @@ +require "test_helper" + +describe Hbc::CLI::Cat do + describe "given a basic Cask" do + before do + @expected_output = <<-CLIOUTPUT.undent + test_cask 'basic-cask' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url 'http://example.com/TestCask.dmg' + homepage 'http://example.com/' + + app 'TestCask.app' + end + CLIOUTPUT + end + + it "displays the Cask file content about the specified Cask" do + lambda { + Hbc::CLI::Cat.run("basic-cask") + }.must_output(@expected_output) + end + + it "throws away additional Cask arguments and uses the first" do + lambda { + Hbc::CLI::Cat.run("basic-cask", "local-caffeine") + }.must_output(@expected_output) + end + + it "throws away stray options" do + lambda { + Hbc::CLI::Cat.run("--notavalidoption", "basic-cask") + }.must_output(@expected_output) + end + end + + it "raises an exception when the Cask does not exist" do + lambda { + Hbc::CLI::Cat.run("notacask") + }.must_raise Hbc::CaskUnavailableError + end + + describe "when no Cask is specified" do + it "raises an exception" do + lambda { + Hbc::CLI::Cat.run + }.must_raise Hbc::CaskUnspecifiedError + end + end + + describe "when no Cask is specified, but an invalid option" do + it "raises an exception" do + lambda { + Hbc::CLI::Cat.run("--notavalidoption") + }.must_raise Hbc::CaskUnspecifiedError + end + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/create_test.rb b/Library/Homebrew/cask/test/cask/cli/create_test.rb new file mode 100644 index 0000000000..85d888cfae --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/create_test.rb @@ -0,0 +1,97 @@ +require "test_helper" + +# monkeypatch for testing +class Hbc::CLI::Create + def self.exec_editor(*command) + editor_commands << command + end + + def self.reset! + @editor_commands = [] + end + + def self.editor_commands + @editor_commands ||= [] + end +end + +describe Hbc::CLI::Create do + before do + Hbc::CLI::Create.reset! + end + + after do + %w[new-cask additional-cask another-cask yet-another-cask feine].each do |cask| + path = Hbc.path(cask) + path.delete if path.exist? + end + end + + it "opens the editor for the specified Cask" do + Hbc::CLI::Create.run("new-cask") + Hbc::CLI::Create.editor_commands.must_equal [ + [Hbc.path("new-cask")], + ] + end + + it "drops a template down for the specified Cask" do + Hbc::CLI::Create.run("new-cask") + template = File.read(Hbc.path("new-cask")) + template.must_equal <<-TEMPLATE.undent + cask 'new-cask' do + version '' + sha256 '' + + url 'https://' + name '' + homepage '' + license :unknown # TODO: change license and remove this comment; ':unknown' is a machine-generated placeholder + + app '' + end + TEMPLATE + end + + it "throws away additional Cask arguments and uses the first" do + Hbc::CLI::Create.run("additional-cask", "another-cask") + Hbc::CLI::Create.editor_commands.must_equal [ + [Hbc.path("additional-cask")], + ] + end + + it "throws away stray options" do + Hbc::CLI::Create.run("--notavalidoption", "yet-another-cask") + Hbc::CLI::Create.editor_commands.must_equal [ + [Hbc.path("yet-another-cask")], + ] + end + + it "raises an exception when the Cask already exists" do + lambda { + Hbc::CLI::Create.run("caffeine") + }.must_raise Hbc::CaskAlreadyCreatedError + end + + it "allows creating Casks that are substrings of existing Casks" do + Hbc::CLI::Create.run("feine") + Hbc::CLI::Create.editor_commands.must_equal [ + [Hbc.path("feine")], + ] + end + + describe "when no Cask is specified" do + it "raises an exception" do + lambda { + Hbc::CLI::Create.run + }.must_raise Hbc::CaskUnspecifiedError + end + end + + describe "when no Cask is specified, but an invalid option" do + it "raises an exception" do + lambda { + Hbc::CLI::Create.run("--notavalidoption") + }.must_raise Hbc::CaskUnspecifiedError + end + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/edit_test.rb b/Library/Homebrew/cask/test/cask/cli/edit_test.rb new file mode 100644 index 0000000000..ebec5f414d --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/edit_test.rb @@ -0,0 +1,58 @@ +require "test_helper" + +# monkeypatch for testing +class Hbc::CLI::Edit + def self.exec_editor(*command) + editor_commands << command + end + + def self.reset! + @editor_commands = [] + end + + def self.editor_commands + @editor_commands ||= [] + end +end + +describe Hbc::CLI::Edit do + before do + Hbc::CLI::Edit.reset! + end + + it "opens the editor for the specified Cask" do + Hbc::CLI::Edit.run("alfred") + Hbc::CLI::Edit.editor_commands.must_equal [ + [Hbc.path("alfred")], + ] + end + + it "throws away additional arguments and uses the first" do + Hbc::CLI::Edit.run("adium", "alfred") + Hbc::CLI::Edit.editor_commands.must_equal [ + [Hbc.path("adium")], + ] + end + + it "raises an exception when the Cask doesnt exist" do + lambda { + Hbc::CLI::Edit.run("notacask") + }.must_raise Hbc::CaskUnavailableError + end + + describe "when no Cask is specified" do + it "raises an exception" do + lambda { + Hbc::CLI::Edit.run + }.must_raise Hbc::CaskUnspecifiedError + end + end + + describe "when no Cask is specified, but an invalid option" do + it "raises an exception" do + lambda { + Hbc::CLI::Edit.run("--notavalidoption") + }.must_raise Hbc::CaskUnspecifiedError + end + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/fetch_test.rb b/Library/Homebrew/cask/test/cask/cli/fetch_test.rb new file mode 100644 index 0000000000..70c25646d6 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/fetch_test.rb @@ -0,0 +1,78 @@ +require "test_helper" + +describe Hbc::CLI::Fetch do + let(:local_transmission) { + Hbc.load("local-transmission") + } + + let(:local_caffeine) { + Hbc.load("local-caffeine") + } + + it "allows download the installer of a Cask" do + shutup do + Hbc::CLI::Fetch.run("local-transmission", "local-caffeine") + end + Hbc::CurlDownloadStrategy.new(local_transmission).cached_location.must_be :exist? + Hbc::CurlDownloadStrategy.new(local_caffeine).cached_location.must_be :exist? + end + + it "prevents double fetch (without nuking existing installation)" do + download_stategy = Hbc::CurlDownloadStrategy.new(local_transmission) + + shutup do + Hbc::Download.new(local_transmission).perform + end + old_ctime = File.stat(download_stategy.cached_location).ctime + + shutup do + Hbc::CLI::Fetch.run("local-transmission") + end + new_ctime = File.stat(download_stategy.cached_location).ctime + + old_ctime.to_i.must_equal new_ctime.to_i + end + + it "allows double fetch with --force" do + shutup do + Hbc::Download.new(local_transmission).perform + end + + download_stategy = Hbc::CurlDownloadStrategy.new(local_transmission) + old_ctime = File.stat(download_stategy.cached_location).ctime + sleep(1) + + shutup do + Hbc::CLI::Fetch.run("local-transmission", "--force") + end + download_stategy = Hbc::CurlDownloadStrategy.new(local_transmission) + new_ctime = File.stat(download_stategy.cached_location).ctime + + # new_ctime.to_i.must_be :>, old_ctime.to_i + new_ctime.to_i.must_be :>, old_ctime.to_i + end + + it "properly handles Casks that are not present" do + lambda { + shutup do + Hbc::CLI::Fetch.run("notacask") + end + }.must_raise Hbc::CaskUnavailableError + end + + describe "when no Cask is specified" do + it "raises an exception" do + lambda { + Hbc::CLI::Fetch.run + }.must_raise Hbc::CaskUnspecifiedError + end + end + + describe "when no Cask is specified, but an invalid option" do + it "raises an exception" do + lambda { + Hbc::CLI::Fetch.run("--notavalidoption") + }.must_raise Hbc::CaskUnspecifiedError + end + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/home_test.rb b/Library/Homebrew/cask/test/cask/cli/home_test.rb new file mode 100644 index 0000000000..28fd2a391c --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/home_test.rb @@ -0,0 +1,44 @@ +require "test_helper" + +# monkeypatch for testing +class Hbc::CLI::Home + def self.system(*command) + system_commands << command + end + + def self.reset! + @system_commands = [] + end + + def self.system_commands + @system_commands ||= [] + end +end + +describe Hbc::CLI::Home do + before do + Hbc::CLI::Home.reset! + end + + it "opens the homepage for the specified Cask" do + Hbc::CLI::Home.run("alfred") + Hbc::CLI::Home.system_commands.must_equal [ + ["/usr/bin/open", "--", "https://www.alfredapp.com/"], + ] + end + + it "works for multiple Casks" do + Hbc::CLI::Home.run("alfred", "adium") + Hbc::CLI::Home.system_commands.must_equal [ + ["/usr/bin/open", "--", "https://www.alfredapp.com/"], + ["/usr/bin/open", "--", "https://www.adium.im/"], + ] + end + + it "opens the project page when no Cask is specified" do + Hbc::CLI::Home.run + Hbc::CLI::Home.system_commands.must_equal [ + ["/usr/bin/open", "--", "http://caskroom.io/"], + ] + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/info_test.rb b/Library/Homebrew/cask/test/cask/cli/info_test.rb new file mode 100644 index 0000000000..441f9d835b --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/info_test.rb @@ -0,0 +1,110 @@ +require "test_helper" + +describe Hbc::CLI::Info do + it "displays some nice info about the specified Cask" do + lambda { + Hbc::CLI::Info.run("local-caffeine") + }.must_output <<-EOS.undent + local-caffeine: 1.2.3 + http://example.com/local-caffeine + Not installed + From: https://github.com/caskroom/homebrew-testcasks/blob/master/Casks/local-caffeine.rb + ==> Name + None + ==> Artifacts + Caffeine.app (app) + EOS + end + + describe "given multiple Casks" do + before do + @expected_output = <<-EOS.undent + local-caffeine: 1.2.3 + http://example.com/local-caffeine + Not installed + From: https://github.com/caskroom/homebrew-testcasks/blob/master/Casks/local-caffeine.rb + ==> Name + None + ==> Artifacts + Caffeine.app (app) + local-transmission: 2.61 + http://example.com/local-transmission + Not installed + From: https://github.com/caskroom/homebrew-testcasks/blob/master/Casks/local-transmission.rb + ==> Name + None + ==> Artifacts + Transmission.app (app) + EOS + end + + it "displays the info" do + lambda { + Hbc::CLI::Info.run("local-caffeine", "local-transmission") + }.must_output(@expected_output) + end + + it "throws away stray options" do + lambda { + Hbc::CLI::Info.run("--notavalidoption", "local-caffeine", "local-transmission") + }.must_output(@expected_output) + end + end + + it "should print caveats if the Cask provided one" do + lambda { + Hbc::CLI::Info.run("with-caveats") + }.must_output <<-EOS.undent + with-caveats: 1.2.3 + http://example.com/local-caffeine + Not installed + From: https://github.com/caskroom/homebrew-testcasks/blob/master/Casks/with-caveats.rb + ==> Name + None + ==> Artifacts + Caffeine.app (app) + ==> Caveats + Here are some things you might want to know. + + Cask token: with-caveats + + Custom text via puts followed by DSL-generated text: + To use with-caveats, you may need to add the /custom/path/bin directory + to your PATH environment variable, eg (for bash shell): + + export PATH=/custom/path/bin:"$PATH" + + EOS + end + + it 'should not print "Caveats" section divider if the caveats block has no output' do + lambda { + Hbc::CLI::Info.run("with-conditional-caveats") + }.must_output <<-EOS.undent + with-conditional-caveats: 1.2.3 + http://example.com/local-caffeine + Not installed + From: https://github.com/caskroom/homebrew-testcasks/blob/master/Casks/with-conditional-caveats.rb + ==> Name + None + ==> Artifacts + Caffeine.app (app) + EOS + end + + describe "when no Cask is specified" do + it "raises an exception" do + lambda { + Hbc::CLI::Info.run + }.must_raise Hbc::CaskUnspecifiedError + end + end + + describe "when no Cask is specified, but an invalid option" do + it "raises an exception" do + lambda { + Hbc::CLI::Info.run("--notavalidoption") + }.must_raise Hbc::CaskUnspecifiedError + end + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/install_test.rb b/Library/Homebrew/cask/test/cask/cli/install_test.rb new file mode 100644 index 0000000000..2d09846b7f --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/install_test.rb @@ -0,0 +1,109 @@ +require "test_helper" + +describe Hbc::CLI::Install do + it "allows staging and activation of multiple Casks at once" do + shutup do + Hbc::CLI::Install.run("local-transmission", "local-caffeine") + end + + Hbc.load("local-transmission").must_be :installed? + Hbc.appdir.join("Transmission.app").must_be :directory? + Hbc.load("local-caffeine").must_be :installed? + Hbc.appdir.join("Caffeine.app").must_be :directory? + end + + it "skips double install (without nuking existing installation)" do + shutup do + Hbc::CLI::Install.run("local-transmission") + end + shutup do + Hbc::CLI::Install.run("local-transmission") + end + Hbc.load("local-transmission").must_be :installed? + end + + it "prints a warning message on double install" do + shutup do + Hbc::CLI::Install.run("local-transmission") + end + + TestHelper.must_output(self, lambda { + Hbc::CLI::Install.run("local-transmission", "") + }, %r{Warning: A Cask for local-transmission is already installed. Add the "--force" option to force re-install.}) + end + + it "allows double install with --force" do + shutup do + Hbc::CLI::Install.run("local-transmission") + end + + TestHelper.must_output(self, lambda { + Hbc::CLI::Install.run("local-transmission", "--force") + }, %r{==> Success! local-transmission was successfully installed!}) + end + + it "skips dependencies with --skip-cask-deps" do + shutup do + Hbc::CLI::Install.run("with-depends-on-cask-multiple", "--skip-cask-deps") + end + Hbc.load("with-depends-on-cask-multiple").must_be :installed? + Hbc.load("local-caffeine").wont_be :installed? + Hbc.load("local-transmission").wont_be :installed? + end + + it "properly handles Casks that are not present" do + lambda { + shutup do + Hbc::CLI::Install.run("notacask") + end + }.must_raise Hbc::CaskError + end + + it "returns a suggestion for a misspelled Cask" do + _, err = capture_io do + begin + Hbc::CLI::Install.run("googlechrome") + rescue Hbc::CaskError + return + end + end + err.must_match %r{No available Cask for googlechrome\. Did you mean:\ngoogle-chrome} + end + + it "returns multiple suggestions for a Cask fragment" do + _, err = capture_io do + begin + Hbc::CLI::Install.run("google") + rescue Hbc::CaskError + return + end + end + err.must_match %r{No available Cask for google\. Did you mean one of:\ngoogle} + end + + describe "when no Cask is specified" do + with_options = lambda do |options| + it "raises an exception" do + lambda { + Hbc::CLI::Install.run(*options) + }.must_raise Hbc::CaskUnspecifiedError + end + end + + describe "without options" do + with_options.call([]) + end + + describe "with --force" do + with_options.call(["--force"]) + end + + describe "with --skip-cask-deps" do + with_options.call(["--skip-cask-deps"]) + end + + describe "with an invalid option" do + with_options.call(["--notavalidoption"]) + end + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/list_test.rb b/Library/Homebrew/cask/test/cask/cli/list_test.rb new file mode 100644 index 0000000000..2189498d41 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/list_test.rb @@ -0,0 +1,88 @@ +require "test_helper" + +describe Hbc::CLI::List do + it "lists the installed Casks in a pretty fashion" do + casks = %w[local-caffeine local-transmission].map { |c| Hbc.load(c) } + + casks.each do |c| + TestHelper.install_with_caskfile(c) + end + + lambda { + Hbc::CLI::List.run + }.must_output <<-EOS.undent + local-caffeine + local-transmission + EOS + end + + describe "lists versions" do + let(:casks) { ["local-caffeine", "local-transmission"] } + let(:output) { + <<-EOS.undent + local-caffeine 1.2.3 + local-transmission 2.61 + EOS + } + + before(:each) do + casks.map(&Hbc.method(:load)).each(&TestHelper.method(:install_with_caskfile)) + end + + it "of all installed Casks" do + lambda { + Hbc::CLI::List.run("--versions") + }.must_output(output) + end + + it "of given Casks" do + lambda { + Hbc::CLI::List.run("--versions", "local-caffeine", "local-transmission") + }.must_output(output) + 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 + + after do + caskroom_path.rmtree + end + + it "lists installed Casks without backing ruby files (due to renames or otherwise)" do + lambda { + Hbc::CLI::List.run + }.must_output <<-EOS.undent + ive-been-renamed (!) + EOS + end + end + + describe "given a set of installed Casks" do + let(:caffeine) { Hbc.load("local-caffeine") } + let(:transmission) { Hbc.load("local-transmission") } + let(:casks) { [caffeine, transmission] } + + it "lists the installed files for those Casks" do + casks.each(&TestHelper.method(:install_without_artifacts_with_caskfile)) + + shutup do + Hbc::Artifact::App.new(transmission).install_phase + end + + lambda { + Hbc::CLI::List.run("local-transmission", "local-caffeine") + }.must_output <<-EOS.undent + ==> Apps + #{Hbc.appdir.join('Transmission.app')} (#{Hbc.appdir.join('Transmission.app').abv}) + ==> Apps + Missing App: #{Hbc.appdir.join('Caffeine.app')} + EOS + end + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/options_test.rb b/Library/Homebrew/cask/test/cask/cli/options_test.rb new file mode 100644 index 0000000000..32bd2e2d71 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/options_test.rb @@ -0,0 +1,130 @@ +require "test_helper" + +describe Hbc::CLI do + it "supports setting the appdir" do + Hbc::CLI.process_options %w[help --appdir=/some/path/foo] + + Hbc.appdir.must_equal Pathname("/some/path/foo") + end + + it "supports setting the appdir from ENV" do + ENV["HOMEBREW_CASK_OPTS"] = "--appdir=/some/path/bar" + + Hbc::CLI.process_options %w[help] + + Hbc.appdir.must_equal Pathname("/some/path/bar") + end + + it "supports setting the prefpanedir" do + Hbc::CLI.process_options %w[help --prefpanedir=/some/path/foo] + + Hbc.prefpanedir.must_equal Pathname("/some/path/foo") + end + + it "supports setting the prefpanedir from ENV" do + ENV["HOMEBREW_CASK_OPTS"] = "--prefpanedir=/some/path/bar" + + Hbc::CLI.process_options %w[help] + + Hbc.prefpanedir.must_equal Pathname("/some/path/bar") + end + + it "supports setting the qlplugindir" do + Hbc::CLI.process_options %w[help --qlplugindir=/some/path/foo] + + Hbc.qlplugindir.must_equal Pathname("/some/path/foo") + end + + it "supports setting the qlplugindir from ENV" do + ENV["HOMEBREW_CASK_OPTS"] = "--qlplugindir=/some/path/bar" + + Hbc::CLI.process_options %w[help] + + Hbc.qlplugindir.must_equal Pathname("/some/path/bar") + end + + it "supports setting the colorpickerdir" do + Hbc::CLI.process_options %w[help --colorpickerdir=/some/path/foo] + + Hbc.colorpickerdir.must_equal Pathname("/some/path/foo") + end + + it "supports setting the colorpickerdir from ENV" do + ENV["HOMEBREW_CASK_OPTS"] = "--colorpickerdir=/some/path/bar" + + Hbc::CLI.process_options %w[help] + + Hbc.colorpickerdir.must_equal Pathname("/some/path/bar") + end + + it "supports setting the fontdir" do + Hbc::CLI.process_options %w[help --fontdir=/some/path/foo] + + Hbc.fontdir.must_equal Pathname("/some/path/foo") + end + + it "supports setting the fontdir from ENV" do + ENV["HOMEBREW_CASK_OPTS"] = "--fontdir=/some/path/bar" + + Hbc::CLI.process_options %w[help] + + Hbc.fontdir.must_equal Pathname("/some/path/bar") + end + + it "supports setting the servicedir" do + Hbc::CLI.process_options %w[help --servicedir=/some/path/foo] + + Hbc.servicedir.must_equal Pathname("/some/path/foo") + end + + it "supports setting the servicedir from ENV" do + ENV["HOMEBREW_CASK_OPTS"] = "--servicedir=/some/path/bar" + + Hbc::CLI.process_options %w[help] + + Hbc.servicedir.must_equal Pathname("/some/path/bar") + end + + it "allows additional options to be passed through" do + rest = Hbc::CLI.process_options %w[edit foo --create --appdir=/some/path/qux] + + Hbc.appdir.must_equal Pathname("/some/path/qux") + rest.must_equal %w[edit foo --create] + end + + describe "when a mandatory argument is missing" do + it "shows a user-friendly error message" do + lambda { + Hbc::CLI.process_options %w[install -f] + }.must_raise Hbc::CaskError + end + end + + describe "given an ambiguous option" do + it "shows a user-friendly error message" do + lambda { + Hbc::CLI.process_options %w[edit -c] + }.must_raise Hbc::CaskError + end + end + + describe "--debug" do + it "sets the Cask debug method to true" do + Hbc::CLI.process_options %w[help --debug] + Hbc.debug.must_equal true + Hbc.debug = false + end + end + + describe "--help" do + it "sets the Cask help method to true" do + Hbc::CLI.process_options %w[foo --help] + Hbc.help.must_equal true + Hbc.help = false + end + end + + after do + ENV["HOMEBREW_CASK_OPTS"] = nil + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/search_test.rb b/Library/Homebrew/cask/test/cask/cli/search_test.rb new file mode 100644 index 0000000000..a0365862ed --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/search_test.rb @@ -0,0 +1,59 @@ +require "test_helper" + +describe Hbc::CLI::Search do + it "lists the available Casks that match the search term" do + lambda { + Hbc::CLI::Search.run("photoshop") + }.must_output <<-OUTPUT.gsub(%r{^ *}, "") + ==> Partial matches + adobe-photoshop-cc + adobe-photoshop-lightroom + OUTPUT + end + + it "shows that there are no Casks matching a search term that did not result in anything" do + lambda { + Hbc::CLI::Search.run("foo-bar-baz") + }.must_output("No Cask found for \"foo-bar-baz\".\n") + end + + it "lists all available Casks with no search term" do + out = capture_io { Hbc::CLI::Search.run }[0] + out.must_match(%r{google-chrome}) + out.length.must_be :>, 1000 + end + + it "ignores hyphens in search terms" do + out = capture_io { Hbc::CLI::Search.run("goo-gle-chrome") }[0] + out.must_match(%r{google-chrome}) + out.length.must_be :<, 100 + end + + it "ignores hyphens in Cask tokens" do + out = capture_io { Hbc::CLI::Search.run("googlechrome") }[0] + out.must_match(%r{google-chrome}) + out.length.must_be :<, 100 + end + + it "accepts multiple arguments" do + out = capture_io { Hbc::CLI::Search.run("google chrome") }[0] + out.must_match(%r{google-chrome}) + out.length.must_be :<, 100 + end + + it "accepts a regexp argument" do + lambda { + Hbc::CLI::Search.run("/^google-c[a-z]rome$/") + }.must_output "==> Regexp matches\ngoogle-chrome\n" + end + + it "Returns both exact and partial matches" do + out = capture_io { Hbc::CLI::Search.run("mnemosyne") }[0] + out.must_match(%r{^==> Exact match\nmnemosyne\n==> Partial matches\nsubclassed-mnemosyne}) + end + + it "does not search the Tap name" do + out = capture_io { Hbc::CLI::Search.run("caskroom") }[0] + out.must_match(%r{^No Cask found for "caskroom"\.\n}) + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/uninstall_test.rb b/Library/Homebrew/cask/test/cask/cli/uninstall_test.rb new file mode 100644 index 0000000000..e2909873a6 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/uninstall_test.rb @@ -0,0 +1,154 @@ +require "test_helper" + +describe Hbc::CLI::Uninstall do + it "shows an error when a bad Cask is provided" do + lambda { + Hbc::CLI::Uninstall.run("notacask") + }.must_raise Hbc::CaskUnavailableError + end + + it "shows an error when a Cask is provided that's not installed" do + lambda { + Hbc::CLI::Uninstall.run("anvil") + }.must_raise Hbc::CaskNotInstalledError + end + + it "tries anyway on a non-present Cask when --force is given" do + lambda do + Hbc::CLI::Uninstall.run("anvil", "--force") + end # wont_raise + end + + it "can uninstall and unlink multiple Casks at once" do + caffeine = Hbc.load("local-caffeine") + transmission = Hbc.load("local-transmission") + + shutup do + Hbc::Installer.new(caffeine).install + Hbc::Installer.new(transmission).install + end + + caffeine.must_be :installed? + transmission.must_be :installed? + + shutup do + Hbc::CLI::Uninstall.run("local-caffeine", "local-transmission") + end + + caffeine.wont_be :installed? + File.exist?(Hbc.appdir.join("Transmission.app")).must_equal false + transmission.wont_be :installed? + File.exist?(Hbc.appdir.join("Caffeine.app")).must_equal false + end + + describe "when multiple versions of a cask are installed" do + let(:token) { "versioned-cask" } + let(:first_installed_version) { "1.2.3" } + let(:last_installed_version) { "4.5.6" } + let(:timestamped_versions) { + [ + [first_installed_version, "123000"], + [last_installed_version, "456000"], + ] + } + let(:caskroom_path) { Hbc.caskroom.join(token).tap(&:mkpath) } + + before(:each) do + timestamped_versions.each do |timestamped_version| + caskroom_path.join(".metadata", *timestamped_version, "Casks").tap(&:mkpath) + .join("#{token}.rb").open("w") do |caskfile| + caskfile.puts <<-EOF.undent + cask '#{token}' do + version '#{timestamped_version[0]}' + end + EOF + end + caskroom_path.join(timestamped_version[0]).mkpath + end + end + + after(:each) do + caskroom_path.rmtree if caskroom_path.exist? + end + + it "uninstalls one version at a time" do + shutup do + Hbc::CLI::Uninstall.run("versioned-cask") + end + + caskroom_path.join(first_installed_version).must_be :exist? + caskroom_path.join(last_installed_version).wont_be :exist? + caskroom_path.must_be :exist? + + shutup do + Hbc::CLI::Uninstall.run("versioned-cask") + end + + caskroom_path.join(first_installed_version).wont_be :exist? + caskroom_path.wont_be :exist? + end + + it "displays a message when versions remain installed" do + out, err = capture_io do + Hbc::CLI::Uninstall.run("versioned-cask") + end + + out.must_match(%r{#{token} #{first_installed_version} is still installed.}) + err.must_be :empty? + end + end + + describe "when Casks in Taps have been renamed or removed" do + let(:app) { Hbc.appdir.join("ive-been-renamed.app") } + let(:caskroom_path) { Hbc.caskroom.join("ive-been-renamed").tap(&:mkpath) } + let(:saved_caskfile) { caskroom_path.join(".metadata", "latest", "timestamp", "Casks").join("ive-been-renamed.rb") } + + before do + app.tap(&:mkpath) + .join("Contents").tap(&:mkpath) + .join("Info.plist").tap(&FileUtils.method(:touch)) + + caskroom_path.mkpath + + saved_caskfile.dirname.mkpath + + IO.write saved_caskfile, <<-EOF.undent + cask 'ive-been-renamed' do + version :latest + + app 'ive-been-renamed.app' + end + EOF + end + + after do + app.rmtree if app.exist? + caskroom_path.rmtree if caskroom_path.exist? + end + + it "can still uninstall those Casks" do + shutup do + Hbc::CLI::Uninstall.run("ive-been-renamed") + end + + app.wont_be :exist? + caskroom_path.wont_be :exist? + end + end + + describe "when no Cask is specified" do + it "raises an exception" do + lambda { + Hbc::CLI::Uninstall.run + }.must_raise Hbc::CaskUnspecifiedError + end + end + + describe "when no Cask is specified, but an invalid option" do + it "raises an exception" do + lambda { + Hbc::CLI::Uninstall.run("--notavalidoption") + }.must_raise Hbc::CaskUnspecifiedError + end + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/version_test.rb b/Library/Homebrew/cask/test/cask/cli/version_test.rb new file mode 100644 index 0000000000..b451780c18 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/version_test.rb @@ -0,0 +1,9 @@ +require "test_helper" + +describe "brew cask --version" do + it "respects the --version argument" do + lambda { + Hbc::CLI::NullCommand.new("--version").run + }.must_output "#{Hbc.full_version}\n" + end +end diff --git a/Library/Homebrew/cask/test/cask/cli/zap_test.rb b/Library/Homebrew/cask/test/cask/cli/zap_test.rb new file mode 100644 index 0000000000..a9e862b119 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/cli/zap_test.rb @@ -0,0 +1,75 @@ +require "test_helper" + +describe Hbc::CLI::Zap do + it "shows an error when a bad Cask is provided" do + lambda { + Hbc::CLI::Zap.run("notacask") + }.must_raise Hbc::CaskUnavailableError + end + + it "can zap and unlink multiple Casks at once" do + caffeine = Hbc.load("local-caffeine") + transmission = Hbc.load("local-transmission") + + shutup do + Hbc::Installer.new(caffeine).install + Hbc::Installer.new(transmission).install + end + + caffeine.must_be :installed? + transmission.must_be :installed? + + shutup do + Hbc::CLI::Zap.run("--notavalidoption", + "local-caffeine", "local-transmission") + end + + caffeine.wont_be :installed? + Hbc.appdir.join("Transmission.app").wont_be :symlink? + transmission.wont_be :installed? + Hbc.appdir.join("Caffeine.app").wont_be :symlink? + end + + # TODO: Explicit test that both zap and uninstall directives get dispatched. + # The above tests that implicitly. + # + # it "dispatches both uninstall and zap stanzas" do + # with_zap = Hbc.load('with-zap') + # + # shutup do + # Hbc::Installer.new(with_zap).install + # end + # + # with_zap.must_be :installed? + # + # Hbc::FakeSystemCommand.stubs_command(['/usr/bin/sudo', '-E', '--', '/usr/bin/osascript', '-e', 'tell application "System Events" to count processes whose bundle identifier is "my.fancy.package.app"'], '1') + # Hbc::FakeSystemCommand.stubs_command(['/usr/bin/sudo', '-E', '--', '/usr/bin/osascript', '-e', 'tell application id "my.fancy.package.app" to quit']) + # Hbc::FakeSystemCommand.stubs_command(['/usr/bin/sudo', '-E', '--', '/usr/bin/osascript', '-e', 'tell application "System Events" to count processes whose bundle identifier is "my.fancy.package.app.from.uninstall"'], '1') + # Hbc::FakeSystemCommand.stubs_command(['/usr/bin/sudo', '-E', '--', '/usr/bin/osascript', '-e', 'tell application id "my.fancy.package.app.from.uninstall" to quit']) + # + # Hbc::FakeSystemCommand.expects_command(['/usr/bin/sudo', '-E', '--', with_zap.staged_path.join('MyFancyPkg','FancyUninstaller.tool'), '--please']) + # Hbc::FakeSystemCommand.expects_command(['/usr/bin/sudo', '-E', '--', '/bin/rm', '-rf', '--', + # Pathname.new('~/Library/Preferences/my.fancy.app.plist').expand_path]) + # + # shutup do + # Hbc::CLI::Zap.run('with-zap') + # end + # with_zap.wont_be :installed? + # end + + describe "when no Cask is specified" do + it "raises an exception" do + lambda { + Hbc::CLI::Zap.run + }.must_raise Hbc::CaskUnspecifiedError + end + end + + describe "when no Cask is specified, but an invalid option" do + it "raises an exception" do + lambda { + Hbc::CLI::Zap.run("--notavalidoption") + }.must_raise Hbc::CaskUnspecifiedError + end + end +end diff --git a/Library/Homebrew/cask/test/cask/container/dmg_test.rb b/Library/Homebrew/cask/test/cask/container/dmg_test.rb new file mode 100644 index 0000000000..2c33b88b65 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/container/dmg_test.rb @@ -0,0 +1,22 @@ +require "test_helper" + +describe Hbc::Container::Dmg do + describe "mount!" do + it "does not store nil mounts for dmgs with extra data" do + transmission = Hbc.load("local-transmission") + + dmg = Hbc::Container::Dmg.new( + transmission, + Pathname(transmission.url.path), + Hbc::SystemCommand + ) + + begin + dmg.mount! + dmg.mounts.wont_include nil + ensure + dmg.eject! + end + end + end +end diff --git a/Library/Homebrew/cask/test/cask/container/naked_test.rb b/Library/Homebrew/cask/test/cask/container/naked_test.rb new file mode 100644 index 0000000000..d40c16de5f --- /dev/null +++ b/Library/Homebrew/cask/test/cask/container/naked_test.rb @@ -0,0 +1,20 @@ +require "test_helper" + +describe Hbc::Container::Naked do + it "saves files with spaces in them from uris with encoded spaces" do + cask = Hbc::Cask.new("spacey") do + url "http://example.com/kevin%20spacey.pkg" + version "1.2" + end + + path = "/tmp/downloads/kevin-spacey-1.2.pkg" + expected_destination = cask.staged_path.join("kevin spacey.pkg") + expected_command = ["/usr/bin/ditto", "--", path, expected_destination] + Hbc::FakeSystemCommand.stubs_command(expected_command) + + container = Hbc::Container::Naked.new(cask, path, Hbc::FakeSystemCommand) + container.extract + + Hbc::FakeSystemCommand.system_calls[expected_command].must_equal 1 + end +end diff --git a/Library/Homebrew/cask/test/cask/depends_on_test.rb b/Library/Homebrew/cask/test/cask/depends_on_test.rb new file mode 100644 index 0000000000..31e51b5e5e --- /dev/null +++ b/Library/Homebrew/cask/test/cask/depends_on_test.rb @@ -0,0 +1,121 @@ +require "test_helper" + +# TODO: this test should be named after the corresponding class, once +# that class is abstracted from installer.rb +describe "Satisfy Dependencies and Requirements" do + # TODO: test that depends_on formula invokes Homebrew + # + # describe "depends_on formula" do + # it "" do + # end + # end + # + + describe "depends_on cask" do + it "raises an exception when depends_on cask is cyclic" do + dep_cask = Hbc.load("with-depends-on-cask-cyclic") + lambda { + shutup do + Hbc::Installer.new(dep_cask).install + end + }.must_raise(Hbc::CaskCyclicCaskDependencyError) + end + + it "installs the dependency of a Cask and the Cask itself" do + csk = Hbc.load("with-depends-on-cask") + dependency = Hbc.load(csk.depends_on.cask.first) + shutup do + Hbc::Installer.new(csk).install + end + + csk.must_be :installed? + dependency.must_be :installed? + end + end + + describe "depends_on macos" do + it "understands depends_on macos: " do + macos_cask = Hbc.load("with-depends-on-macos-array") + shutup do + Hbc::Installer.new(macos_cask).install + end + end + + it "understands depends_on macos: " do + macos_cask = Hbc.load("with-depends-on-macos-comparison") + shutup do + Hbc::Installer.new(macos_cask).install + end + end + + it "understands depends_on macos: " do + macos_cask = Hbc.load("with-depends-on-macos-string") + shutup do + Hbc::Installer.new(macos_cask).install + end + end + + it "understands depends_on macos: " do + macos_cask = Hbc.load("with-depends-on-macos-symbol") + shutup do + Hbc::Installer.new(macos_cask).install + end + end + + it "raises an exception when depends_on macos is not satisfied" do + macos_cask = Hbc.load("with-depends-on-macos-failure") + lambda { + shutup do + Hbc::Installer.new(macos_cask).install + end + }.must_raise(Hbc::CaskError) + end + end + + describe "depends_on arch" do + it "succeeds when depends_on arch is satisfied" do + arch_cask = Hbc.load("with-depends-on-arch") + shutup do + Hbc::Installer.new(arch_cask).install + end + end + + it "raises an exception when depends_on arch is not satisfied" do + arch_cask = Hbc.load("with-depends-on-arch-failure") + lambda { + shutup do + Hbc::Installer.new(arch_cask).install + end + }.must_raise(Hbc::CaskError) + end + end + + describe "depends_on x11" do + it "succeeds when depends_on x11 is satisfied" do + x11_cask = Hbc.load("with-depends-on-x11") + shutup do + Hbc::Installer.new(x11_cask).install + end + end + + it "raises an exception when depends_on x11 is not satisfied" do + x11_cask = Hbc.load("with-depends-on-x11") + Hbc.stubs(:x11_libpng).returns([Pathname.new("/usr/path/does/not/exist")]) + lambda { + shutup do + Hbc::Installer.new(x11_cask).install + end + }.must_raise(Hbc::CaskX11DependencyError) + end + + it "never raises when depends_on x11: false" do + x11_cask = Hbc.load("with-depends-on-x11-false") + Hbc.stubs(:x11_executable).returns(Pathname.new("/usr/path/does/not/exist")) + lambda do + shutup do + Hbc::Installer.new(x11_cask).install + end + end # won't raise + end + end +end diff --git a/Library/Homebrew/cask/test/cask/dsl/caveats_test.rb b/Library/Homebrew/cask/test/cask/dsl/caveats_test.rb new file mode 100644 index 0000000000..d0c7eef342 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/dsl/caveats_test.rb @@ -0,0 +1,10 @@ +require "test_helper" + +describe Hbc::DSL::Caveats do + let(:cask) { Hbc.load("basic-cask") } + let(:dsl) { Hbc::DSL::Caveats.new(cask) } + + it_behaves_like Hbc::DSL::Base + + # TODO: add tests for Caveats DSL methods +end diff --git a/Library/Homebrew/cask/test/cask/dsl/postflight_test.rb b/Library/Homebrew/cask/test/cask/dsl/postflight_test.rb new file mode 100644 index 0000000000..c5e80b6d08 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/dsl/postflight_test.rb @@ -0,0 +1,12 @@ +require "test_helper" + +describe Hbc::DSL::Postflight do + let(:cask) { Hbc.load("basic-cask") } + let(:dsl) { Hbc::DSL::Postflight.new(cask, Hbc::FakeSystemCommand) } + + it_behaves_like Hbc::DSL::Base + + it_behaves_like Hbc::Staged do + let(:staged) { dsl } + end +end diff --git a/Library/Homebrew/cask/test/cask/dsl/preflight_test.rb b/Library/Homebrew/cask/test/cask/dsl/preflight_test.rb new file mode 100644 index 0000000000..1c49a62ad9 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/dsl/preflight_test.rb @@ -0,0 +1,12 @@ +require "test_helper" + +describe Hbc::DSL::Preflight do + let(:cask) { Hbc.load("basic-cask") } + let(:dsl) { Hbc::DSL::Preflight.new(cask, Hbc::FakeSystemCommand) } + + it_behaves_like Hbc::DSL::Base + + it_behaves_like Hbc::Staged do + let(:staged) { dsl } + end +end diff --git a/Library/Homebrew/cask/test/cask/dsl/uninstall_postflight_test.rb b/Library/Homebrew/cask/test/cask/dsl/uninstall_postflight_test.rb new file mode 100644 index 0000000000..c704706adf --- /dev/null +++ b/Library/Homebrew/cask/test/cask/dsl/uninstall_postflight_test.rb @@ -0,0 +1,8 @@ +require "test_helper" + +describe Hbc::DSL::UninstallPostflight do + let(:cask) { Hbc.load("basic-cask") } + let(:dsl) { Hbc::DSL::UninstallPostflight.new(cask, Hbc::FakeSystemCommand) } + + it_behaves_like Hbc::DSL::Base +end diff --git a/Library/Homebrew/cask/test/cask/dsl/uninstall_preflight_test.rb b/Library/Homebrew/cask/test/cask/dsl/uninstall_preflight_test.rb new file mode 100644 index 0000000000..f6ab36b60c --- /dev/null +++ b/Library/Homebrew/cask/test/cask/dsl/uninstall_preflight_test.rb @@ -0,0 +1,12 @@ +require "test_helper" + +describe Hbc::DSL::UninstallPreflight do + let(:cask) { Hbc.load("basic-cask") } + let(:dsl) { Hbc::DSL::UninstallPreflight.new(cask, Hbc::FakeSystemCommand) } + + it_behaves_like Hbc::DSL::Base + + it_behaves_like Hbc::Staged do + let(:staged) { dsl } + end +end diff --git a/Library/Homebrew/cask/test/cask/dsl_test.rb b/Library/Homebrew/cask/test/cask/dsl_test.rb new file mode 100644 index 0000000000..4cd30d2414 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/dsl_test.rb @@ -0,0 +1,431 @@ +require "test_helper" + +describe Hbc::DSL do + it "lets you set url, homepage, and version" do + test_cask = Hbc.load("basic-cask") + test_cask.url.to_s.must_equal "http://example.com/TestCask.dmg" + test_cask.homepage.must_equal "http://example.com/" + test_cask.version.to_s.must_equal "1.2.3" + end + + describe "when a Cask includes an unknown method" do + attempt_unknown_method = nil + + before do + attempt_unknown_method = lambda do + Hbc::Cask.new("unexpected-method-cask") do + future_feature :not_yet_on_your_machine + end + end + end + + it "prints a warning that it has encountered an unexpected method" do + expected = Regexp.compile(<<-EOREGEX.undent.lines.map(&:chomp).join("")) + (?m) + Warning: + .* + Unexpected method 'future_feature' called on Cask unexpected-method-cask\\. + .* + brew update; brew cleanup; brew cask cleanup + .* + https://github.com/caskroom/homebrew-cask#reporting-bugs + EOREGEX + + TestHelper.must_output(self, attempt_unknown_method, expected) + end + + it "will simply warn, not throw an exception" do + begin + capture_subprocess_io do + attempt_unknown_method.call + end + rescue StandardError => e + flunk("Wanted unexpected method to simply warn, but got exception #{e}") + end + end + end + + describe "header line" do + it "requires a valid header format" do + lambda { + Hbc.load("invalid/invalid-header-format") + }.must_raise(SyntaxError) + end + + it "requires the header token to match the file name" do + err = lambda { + Hbc.load("invalid/invalid-header-token-mismatch") + }.must_raise(Hbc::CaskTokenDoesNotMatchError) + err.message.must_include "Bad header line:" + err.message.must_include "does not match file name" + end + + it "does not require a DSL version in the header" do + test_cask = Hbc.load("no-dsl-version") + test_cask.url.to_s.must_equal "http://example.com/TestCask.dmg" + test_cask.homepage.must_equal "http://example.com/" + test_cask.version.to_s.must_equal "1.2.3" + end + end + + describe "name stanza" do + it "lets you set the full name via a name stanza" do + cask = Hbc::Cask.new("name-cask") do + name "Proper Name" + end + + cask.name.must_equal [ + "Proper Name", + ] + end + + it "Accepts an array value to the name stanza" do + cask = Hbc::Cask.new("array-name-cask") do + name ["Proper Name", "Alternate Name"] + end + + cask.name.must_equal [ + "Proper Name", + "Alternate Name", + ] + end + + it "Accepts multiple name stanzas" do + cask = Hbc::Cask.new("multi-name-cask") do + name "Proper Name" + name "Alternate Name" + end + + cask.name.must_equal [ + "Proper Name", + "Alternate Name", + ] + end + end + + describe "sha256 stanza" do + it "lets you set checksum via sha256" do + cask = Hbc::Cask.new("checksum-cask") do + sha256 "imasha2" + end + + cask.sha256.must_equal "imasha2" + end + end + + describe "app stanza" do + it "allows you to specify app stanzas" do + cask = Hbc::Cask.new("cask-with-apps") do + app "Foo.app" + app "Bar.app" + end + + Array(cask.artifacts[:app]).must_equal [["Foo.app"], ["Bar.app"]] + end + + it "allow app stanzas to be empty" do + cask = Hbc::Cask.new("cask-with-no-apps") + Array(cask.artifacts[:app]).must_equal %w[] + end + end + + describe "caveats stanza" do + it "allows caveats to be specified via a method define" do + cask = Hbc::Cask.new("plain-cask") + + cask.caveats.must_be :empty? + + cask = Hbc::Cask.new("cask-with-caveats") do + def caveats; <<-EOS.undent + When you install this Cask, you probably want to know this. + EOS + end + end + + cask.caveats.must_equal "When you install this Cask, you probably want to know this.\n" + end + end + + describe "pkg stanza" do + it "allows installable pkgs to be specified" do + cask = Hbc::Cask.new("cask-with-pkgs") do + pkg "Foo.pkg" + pkg "Bar.pkg" + end + + Array(cask.artifacts[:pkg]).must_equal [["Foo.pkg"], ["Bar.pkg"]] + end + end + + describe "url stanza" do + it "prevents defining multiple urls" do + err = lambda { + Hbc.load("invalid/invalid-two-url") + }.must_raise(Hbc::CaskInvalidError) + err.message.must_include "'url' stanza may only appear once" + end + end + + describe "homepage stanza" do + it "prevents defining multiple homepages" do + err = lambda { + Hbc.load("invalid/invalid-two-homepage") + }.must_raise(Hbc::CaskInvalidError) + err.message.must_include "'homepage' stanza may only appear once" + end + end + + describe "version stanza" do + it "prevents defining multiple versions" do + err = lambda { + Hbc.load("invalid/invalid-two-version") + }.must_raise(Hbc::CaskInvalidError) + err.message.must_include "'version' stanza may only appear once" + end + end + + describe "appcast stanza" do + it "allows appcasts to be specified" do + cask = Hbc.load("with-appcast") + cask.appcast.to_s.must_match %r{^http} + end + + it "prevents defining multiple appcasts" do + err = lambda { + Hbc.load("invalid/invalid-appcast-multiple") + }.must_raise(Hbc::CaskInvalidError) + err.message.must_include "'appcast' stanza may only appear once" + end + + it "refuses to load invalid appcast URLs" do + lambda { + Hbc.load("invalid/invalid-appcast-url") + }.must_raise(Hbc::CaskInvalidError) + end + end + + describe "gpg stanza" do + it "allows gpg stanza to be specified" do + cask = Hbc.load("with-gpg") + cask.gpg.to_s.must_match %r{\S} + end + + it "allows gpg stanza to be specified with :key_url" do + cask = Hbc.load("with-gpg-key-url") + cask.gpg.to_s.must_match %r{\S} + end + + it "prevents specifying gpg stanza multiple times" do + err = lambda { + Hbc.load("invalid/invalid-gpg-multiple-stanzas") + }.must_raise(Hbc::CaskInvalidError) + err.message.must_include "'gpg' stanza may only appear once" + end + + it "prevents missing gpg key parameters" do + err = lambda { + Hbc.load("invalid/invalid-gpg-missing-key") + }.must_raise(Hbc::CaskInvalidError) + err.message.must_include "'gpg' stanza must include exactly one" + end + + it "prevents conflicting gpg key parameters" do + err = lambda { + Hbc.load("invalid/invalid-gpg-conflicting-keys") + }.must_raise(Hbc::CaskInvalidError) + err.message.must_include "'gpg' stanza must include exactly one" + end + + it "refuses to load invalid gpg signature URLs" do + lambda { + Hbc.load("invalid/invalid-gpg-signature-url") + }.must_raise(Hbc::CaskInvalidError) + end + + it "refuses to load invalid gpg key URLs" do + lambda { + Hbc.load("invalid/invalid-gpg-key-url") + }.must_raise(Hbc::CaskInvalidError) + end + + it "refuses to load invalid gpg key IDs" do + lambda { + Hbc.load("invalid/invalid-gpg-key-id") + }.must_raise(Hbc::CaskInvalidError) + end + + it "refuses to load if gpg parameter is unknown" do + lambda { + Hbc.load("invalid/invalid-gpg-parameter") + }.must_raise(Hbc::CaskInvalidError) + end + end + + describe "depends_on stanza" do + it "refuses to load with an invalid depends_on key" do + lambda { + Hbc.load("invalid/invalid-depends-on-key") + }.must_raise(Hbc::CaskInvalidError) + end + end + + describe "depends_on formula" do + it "allows depends_on formula to be specified" do + cask = Hbc.load("with-depends-on-formula") + cask.depends_on.formula.wont_be_nil + end + + it "allows multiple depends_on formula to be specified" do + cask = Hbc.load("with-depends-on-formula-multiple") + cask.depends_on.formula.wont_be_nil + end + end + + describe "depends_on cask" do + it "allows depends_on cask to be specified" do + cask = Hbc.load("with-depends-on-cask") + cask.depends_on.cask.wont_be_nil + end + + it "allows multiple depends_on cask to be specified" do + cask = Hbc.load("with-depends-on-cask-multiple") + cask.depends_on.cask.wont_be_nil + end + end + + describe "depends_on macos" do + it "allows depends_on macos to be specified" do + cask = Hbc.load("with-depends-on-macos-string") + cask.depends_on.macos.wont_be_nil + end + it "refuses to load with an invalid depends_on macos value" do + lambda { + Hbc.load("invalid/invalid-depends-on-macos-bad-release") + }.must_raise(Hbc::CaskInvalidError) + end + it "refuses to load with conflicting depends_on macos forms" do + lambda { + Hbc.load("invalid/invalid-depends-on-macos-conflicting-forms") + }.must_raise(Hbc::CaskInvalidError) + end + end + + describe "depends_on arch" do + it "allows depends_on arch to be specified" do + cask = Hbc.load("with-depends-on-arch") + cask.depends_on.arch.wont_be_nil + end + it "refuses to load with an invalid depends_on arch value" do + lambda { + Hbc.load("invalid/invalid-depends-on-arch-value") + }.must_raise(Hbc::CaskInvalidError) + end + end + + describe "depends_on x11" do + it "allows depends_on x11 to be specified" do + cask = Hbc.load("with-depends-on-x11") + cask.depends_on.x11.wont_be_nil + end + it "refuses to load with an invalid depends_on x11 value" do + lambda { + Hbc.load("invalid/invalid-depends-on-x11-value") + }.must_raise(Hbc::CaskInvalidError) + end + end + + describe "conflicts_with stanza" do + it "allows conflicts_with stanza to be specified" do + cask = Hbc.load("with-conflicts-with") + cask.conflicts_with.formula.wont_be_nil + end + + it "refuses to load invalid conflicts_with key" do + lambda { + Hbc.load("invalid/invalid-conflicts-with-key") + }.must_raise(Hbc::CaskInvalidError) + end + end + + describe "license stanza" do + it "allows the license to be specified" do + cask = Hbc.load("with-license") + cask.license.value.must_equal :gpl + end + + it "the license has a category" do + cask = Hbc.load("with-license") + cask.license.category.must_equal :oss + end + + it "prevents defining multiple license stanzas" do + err = lambda { + Hbc.load("invalid/invalid-license-multiple") + }.must_raise(Hbc::CaskInvalidError) + err.message.must_include "'license' stanza may only appear once" + end + + it "refuses to load on invalid license value" do + lambda { + Hbc.load("invalid/invalid-license-value") + }.must_raise(Hbc::CaskInvalidError) + end + end + + describe "installer stanza" do + it "allows installer script to be specified" do + cask = Hbc.load("with-installer-script") + cask.artifacts[:installer].first.script[:executable].must_equal "/usr/bin/true" + cask.artifacts[:installer].first.script[:args].must_equal ["--flag"] + cask.artifacts[:installer].to_a[1].script[:executable].must_equal "/usr/bin/false" + cask.artifacts[:installer].to_a[1].script[:args].must_equal ["--flag"] + end + it "allows installer manual to be specified" do + cask = Hbc.load("with-installer-manual") + cask.artifacts[:installer].first.manual.must_equal "Caffeine.app" + end + end + + describe "stage_only stanza" do + it "allows stage_only stanza to be specified" do + cask = Hbc.load("stage-only") + cask.artifacts[:stage_only].first.must_equal [true] + end + + it "prevents specifying stage_only with other activatables" do + err = lambda { + Hbc.load("invalid/invalid-stage-only-conflict") + }.must_raise(Hbc::CaskInvalidError) + err.message.must_include "'stage_only' must be the only activatable artifact" + end + end + + describe "auto_updates stanza" do + it "allows auto_updates stanza to be specified" do + cask = Hbc.load("auto-updates") + cask.auto_updates.must_equal true + end + end + + describe "appdir" do + it "allows interpolation of the appdir value in stanzas" do + cask = Hbc.load("appdir-interpolation") + cask.artifacts[:binary].first.must_equal ["#{Hbc.appdir}/some/path"] + end + + it "does not include a trailing slash" do + original_appdir = Hbc.appdir + Hbc.appdir = "#{original_appdir}/" + + begin + cask = Hbc::Cask.new("appdir-trailing-slash") do + binary "#{appdir}/some/path" + end + + cask.artifacts[:binary].first.must_equal ["#{original_appdir}/some/path"] + ensure + Hbc.appdir = original_appdir + end + end + end +end diff --git a/Library/Homebrew/cask/test/cask/installer_test.rb b/Library/Homebrew/cask/test/cask/installer_test.rb new file mode 100644 index 0000000000..6efd4affc2 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/installer_test.rb @@ -0,0 +1,418 @@ +require "test_helper" + +describe Hbc::Installer do + describe "install" do + it "downloads and installs a nice fresh Cask" do + caffeine = Hbc.load("local-caffeine") + + shutup do + Hbc::Installer.new(caffeine).install + end + + dest_path = Hbc.caskroom.join("local-caffeine", caffeine.version) + dest_path.must_be :directory? + application = Hbc.appdir.join("Caffeine.app") + application.must_be :directory? + end + + it "works with dmg-based Casks" do + transmission = Hbc.load("local-transmission") + + shutup do + Hbc::Installer.new(transmission).install + end + + dest_path = Hbc.caskroom.join("local-transmission", transmission.version) + dest_path.must_be :directory? + application = Hbc.appdir.join("Transmission.app") + application.must_be :directory? + end + + it "works with tar-based Casks" do + tarball = Hbc.load("tarball") + + shutup do + Hbc::Installer.new(tarball).install + end + + dest_path = Hbc.caskroom.join("tarball", tarball.version) + dest_path.must_be :directory? + application = Hbc.appdir.join("Tarball.app") + application.must_be :directory? + end + + it "works with cab-based Casks" do + skip("cabextract not installed") unless Hbc.homebrew_prefix.join("bin", "cabextract").exist? + cab_container = Hbc.load("cab-container") + empty = stub(formula: [], cask: [], macos: nil, arch: nil, x11: nil) + cab_container.stubs(:depends_on).returns(empty) + + shutup do + Hbc::Installer.new(cab_container).install + end + + dest_path = Hbc.caskroom.join("cab-container", cab_container.version) + dest_path.must_be :directory? + application = Hbc.appdir.join("Application.app") + application.must_be :directory? + end + + it "works with Adobe AIR-based Casks" do + skip("Adobe AIR not installed") unless Hbc::Container::Air.installer_exist? + air_container = Hbc.load("adobe-air-container") + + shutup do + Hbc::Installer.new(air_container).install + end + + dest_path = Hbc.caskroom.join("adobe-air-container", air_container.version) + dest_path.must_be :directory? + application = Hbc.appdir.join("GMDesk.app") + application.must_be :directory? + end + + it "works with 7z-based Casks" do + skip("unar not installed") unless Hbc.homebrew_prefix.join("bin", "unar").exist? + sevenzip_container = Hbc.load("sevenzip-container") + empty = stub(formula: [], cask: [], macos: nil, arch: nil, x11: nil) + sevenzip_container.stubs(:depends_on).returns(empty) + + shutup do + Hbc::Installer.new(sevenzip_container).install + end + + dest_path = Hbc.caskroom.join("sevenzip-container", sevenzip_container.version) + dest_path.must_be :directory? + application = Hbc.appdir.join("Application.app") + application.must_be :directory? + end + + it "works with xar-based Casks" do + xar_container = Hbc.load("xar-container") + + shutup do + Hbc::Installer.new(xar_container).install + end + + dest_path = Hbc.caskroom.join("xar-container", xar_container.version) + dest_path.must_be :directory? + application = Hbc.appdir.join("Application.app") + application.must_be :directory? + end + + it "works with Stuffit-based Casks" do + skip("unar not installed") unless Hbc.homebrew_prefix.join("bin", "unar").exist? + stuffit_container = Hbc.load("stuffit-container") + empty = stub(formula: [], cask: [], macos: nil, arch: nil, x11: nil) + stuffit_container.stubs(:depends_on).returns(empty) + + shutup do + Hbc::Installer.new(stuffit_container).install + end + + dest_path = Hbc.caskroom.join("stuffit-container", stuffit_container.version) + dest_path.must_be :directory? + application = Hbc.appdir.join("sheldonmac", "v1.0") + application.must_be :directory? + end + + it "works with RAR-based Casks" do + skip("unar not installed") unless Hbc.homebrew_prefix.join("bin", "unar").exist? + rar_container = Hbc.load("rar-container") + empty = stub(formula: [], cask: [], macos: nil, arch: nil, x11: nil) + rar_container.stubs(:depends_on).returns(empty) + + shutup do + Hbc::Installer.new(rar_container).install + end + + dest_path = Hbc.caskroom.join("rar-container", rar_container.version) + dest_path.must_be :directory? + application = Hbc.appdir.join("Application.app") + application.must_be :directory? + end + + it "works with bz2-based Casks" do + asset = Hbc.load("bzipped-asset") + + shutup do + Hbc::Installer.new(asset).install + end + + dest_path = Hbc.caskroom.join("bzipped-asset", asset.version) + dest_path.must_be :directory? + file = Hbc.appdir.join("bzipped-asset--#{asset.version}") + file.must_be :file? + end + + it "works with pure gz-based Casks" do + asset = Hbc.load("gzipped-asset") + + shutup do + Hbc::Installer.new(asset).install + end + + dest_path = Hbc.caskroom.join("gzipped-asset", asset.version) + dest_path.must_be :directory? + file = Hbc.appdir.join("gzipped-asset--#{asset.version}") + file.must_be :file? + end + + it "works with xz-based Casks" do + skip("unxz not installed") unless Hbc.homebrew_prefix.join("bin", "unxz").exist? + asset = Hbc.load("xzipped-asset") + empty = stub(formula: [], cask: [], macos: nil, arch: nil, x11: nil) + asset.stubs(:depends_on).returns(empty) + + shutup do + Hbc::Installer.new(asset).install + end + + dest_path = Hbc.caskroom.join("xzipped-asset", asset.version) + dest_path.must_be :directory? + file = Hbc.appdir.join("xzipped-asset--#{asset.version}") + file.must_be :file? + end + + it "works with lzma-based Casks" do + skip("unlzma not installed") unless Hbc.homebrew_prefix.join("bin", "unlzma").exist? + asset = Hbc.load("lzma-asset") + empty = stub(formula: [], cask: [], macos: nil, arch: nil, x11: nil) + asset.stubs(:depends_on).returns(empty) + + shutup do + Hbc::Installer.new(asset).install + end + + dest_path = Hbc.caskroom.join("lzma-asset", asset.version) + dest_path.must_be :directory? + file = Hbc.appdir.join("lzma-asset--#{asset.version}") + file.must_be :file? + end + + it "blows up on a bad checksum" do + bad_checksum = Hbc.load("bad-checksum") + lambda { + shutup do + Hbc::Installer.new(bad_checksum).install + end + }.must_raise(Hbc::CaskSha256MismatchError) + end + + it "blows up on a missing checksum" do + missing_checksum = Hbc.load("missing-checksum") + lambda { + shutup do + Hbc::Installer.new(missing_checksum).install + end + }.must_raise(Hbc::CaskSha256MissingError) + end + + it "installs fine if sha256 :no_check is used" do + no_checksum = Hbc.load("no-checksum") + + shutup do + Hbc::Installer.new(no_checksum).install + end + + no_checksum.must_be :installed? + end + + it "fails to install if sha256 :no_check is used with --require-sha" do + no_checksum = Hbc.load("no-checksum") + lambda { + Hbc::Installer.new(no_checksum, require_sha: true).install + }.must_raise(Hbc::CaskNoShasumError) + end + + it "installs fine if sha256 :no_check is used with --require-sha and --force" do + no_checksum = Hbc.load("no-checksum") + + shutup do + Hbc::Installer.new(no_checksum, require_sha: true, force: true).install + end + + no_checksum.must_be :installed? + end + + it "prints caveats if they're present" do + with_caveats = Hbc.load("with-caveats") + TestHelper.must_output(self, lambda { + Hbc::Installer.new(with_caveats).install + }, %r{Here are some things you might want to know}) + with_caveats.must_be :installed? + end + + it "prints installer :manual instructions when present" do + with_installer_manual = Hbc.load("with-installer-manual") + TestHelper.must_output(self, lambda { + Hbc::Installer.new(with_installer_manual).install + }, %r{To complete the installation of Cask with-installer-manual, you must also\nrun the installer at\n\n '#{with_installer_manual.staged_path.join('Caffeine.app')}'}) + with_installer_manual.must_be :installed? + end + + it "does not extract __MACOSX directories from zips" do + with_macosx_dir = Hbc.load("with-macosx-dir") + + shutup do + Hbc::Installer.new(with_macosx_dir).install + end + + with_macosx_dir.staged_path.join("__MACOSX").wont_be :directory? + end + + it "installer method raises an exception when already-installed Casks which auto-update are attempted" do + auto_updates = Hbc.load("auto-updates") + auto_updates.installed?.must_equal false + installer = Hbc::Installer.new(auto_updates) + + shutup do + installer.install + end + + lambda { + installer.install + }.must_raise(Hbc::CaskAutoUpdatesError) + end + + it "allows already-installed Casks which auto-update to be installed if force is provided" do + auto_updates = Hbc.load("auto-updates") + auto_updates.installed?.must_equal false + + shutup do + Hbc::Installer.new(auto_updates).install + end + + shutup do + Hbc::Installer.new(auto_updates, force: true).install + end # wont_raise + end + + # unlike the CLI, the internal interface throws exception on double-install + it "installer method raises an exception when already-installed Casks are attempted" do + transmission = Hbc.load("local-transmission") + transmission.installed?.must_equal false + installer = Hbc::Installer.new(transmission) + + shutup do + installer.install + end + + lambda { + installer.install + }.must_raise(Hbc::CaskAlreadyInstalledError) + end + + it "allows already-installed Casks to be installed if force is provided" do + transmission = Hbc.load("local-transmission") + transmission.installed?.must_equal false + + shutup do + Hbc::Installer.new(transmission).install + end + + shutup do + Hbc::Installer.new(transmission, force: true).install + end # wont_raise + end + + it "works properly with a direct URL to a pkg" do + naked_pkg = Hbc.load("naked-pkg") + + shutup do + Hbc::Installer.new(naked_pkg).install + end + + dest_path = Hbc.caskroom.join("naked-pkg", naked_pkg.version) + pkg = dest_path.join("Naked.pkg") + pkg.must_be :file? + end + + it "works properly with an overridden container :type" do + naked_executable = Hbc.load("naked-executable") + + shutup do + Hbc::Installer.new(naked_executable).install + end + + dest_path = Hbc.caskroom.join("naked-executable", naked_executable.version) + executable = dest_path.join("naked_executable") + executable.must_be :file? + end + + it "works fine with a nested container" do + nested_app = Hbc.load("nested-app") + + shutup do + Hbc::Installer.new(nested_app).install + end + + dest_path = Hbc.appdir.join("MyNestedApp.app") + File.ftype(dest_path).must_equal "directory" + end + + it "generates and finds a timestamped metadata directory for an installed Cask" do + caffeine = Hbc.load("local-caffeine") + + shutup do + Hbc::Installer.new(caffeine).install + end + + m_path = caffeine.metadata_path(:now, true) + caffeine.metadata_path(:now, false).must_equal(m_path) + caffeine.metadata_path(:latest).must_equal(m_path) + end + + it "generates and finds a metadata subdirectory for an installed Cask" do + caffeine = Hbc.load("local-caffeine") + + shutup do + Hbc::Installer.new(caffeine).install + end + + subdir_name = "Casks" + m_subdir = caffeine.metadata_subdir(subdir_name, :now, true) + caffeine.metadata_subdir(subdir_name, :now, false).must_equal(m_subdir) + caffeine.metadata_subdir(subdir_name, :latest).must_equal(m_subdir) + end + end + + describe "uninstall" do + it "fully uninstalls a Cask" do + caffeine = Hbc.load("local-caffeine") + installer = Hbc::Installer.new(caffeine) + + shutup do + installer.install + installer.uninstall + end + + Hbc.caskroom.join("local-caffeine", caffeine.version, "Caffeine.app").wont_be :directory? + Hbc.caskroom.join("local-caffeine", caffeine.version).wont_be :directory? + Hbc.caskroom.join("local-caffeine").wont_be :directory? + end + + it "uninstalls all versions if force is set" do + caffeine = Hbc.load("local-caffeine") + mutated_version = caffeine.version + ".1" + + shutup do + Hbc::Installer.new(caffeine).install + end + + Hbc.caskroom.join("local-caffeine", caffeine.version).must_be :directory? + Hbc.caskroom.join("local-caffeine", mutated_version).wont_be :directory? + FileUtils.mv(Hbc.caskroom.join("local-caffeine", caffeine.version), Hbc.caskroom.join("local-caffeine", mutated_version)) + Hbc.caskroom.join("local-caffeine", caffeine.version).wont_be :directory? + Hbc.caskroom.join("local-caffeine", mutated_version).must_be :directory? + + shutup do + Hbc::Installer.new(caffeine, force: true).uninstall + end + + Hbc.caskroom.join("local-caffeine", caffeine.version).wont_be :directory? + Hbc.caskroom.join("local-caffeine", mutated_version).wont_be :directory? + Hbc.caskroom.join("local-caffeine").wont_be :directory? + end + end +end diff --git a/Library/Homebrew/cask/test/cask/pkg_test.rb b/Library/Homebrew/cask/test/cask/pkg_test.rb new file mode 100644 index 0000000000..5734a58f6a --- /dev/null +++ b/Library/Homebrew/cask/test/cask/pkg_test.rb @@ -0,0 +1,94 @@ +require "test_helper" + +describe Hbc::Pkg do + describe "uninstall" do + it "removes files and dirs referenced by the pkg" do + pkg = Hbc::Pkg.new("my.fake.pkg", Hbc::NeverSudoSystemCommand) + + some_files = Array.new(3) { Pathname(Tempfile.new("testfile").path) } + pkg.stubs(:pkgutil_bom_files).returns some_files + + some_specials = Array.new(3) { Pathname(Tempfile.new("testfile").path) } + pkg.stubs(:pkgutil_bom_specials).returns some_specials + + some_dirs = Array.new(3) { Pathname(Dir.mktmpdir) } + pkg.stubs(:pkgutil_bom_dirs).returns some_dirs + + pkg.stubs(:forget) + + pkg.uninstall + + some_files.each do |file| + file.wont_be :exist? + end + + some_dirs.each do |dir| + dir.wont_be :exist? + end + end + + it "forgets the pkg" do + pkg = Hbc::Pkg.new("my.fake.pkg", Hbc::FakeSystemCommand) + + Hbc::FakeSystemCommand.stubs_command( + ["/usr/sbin/pkgutil", "--only-files", "--files", "my.fake.pkg"] + ) + Hbc::FakeSystemCommand.stubs_command( + ["/usr/sbin/pkgutil", "--only-dirs", "--files", "my.fake.pkg"] + ) + Hbc::FakeSystemCommand.stubs_command( + ["/usr/sbin/pkgutil", "--files", "my.fake.pkg"] + ) + + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/sudo", "-E", "--", "/usr/sbin/pkgutil", "--forget", "my.fake.pkg"] + ) + + pkg.uninstall + end + + it "cleans broken symlinks, but leaves AOK symlinks" do + pkg = Hbc::Pkg.new("my.fake.pkg", Hbc::NeverSudoSystemCommand) + + fake_dir = Pathname(Dir.mktmpdir) + fake_file = fake_dir.join("ima_file").tap { |path| FileUtils.touch(path) } + + intact_symlink = fake_dir.join("intact_symlink").tap { |path| path.make_symlink(fake_file) } + broken_symlink = fake_dir.join("broken_symlink").tap { |path| path.make_symlink("im_nota_file") } + + pkg.stubs(:pkgutil_bom_specials).returns([]) + pkg.stubs(:pkgutil_bom_files).returns([]) + pkg.stubs(:pkgutil_bom_dirs).returns([fake_dir]) + pkg.stubs(:forget) + + pkg.uninstall + + intact_symlink.must_be :exist? + broken_symlink.wont_be :exist? + fake_dir.must_be :exist? + end + + it "snags permissions on ornery dirs, but returns them afterwords" do + pkg = Hbc::Pkg.new("my.fake.pkg", Hbc::NeverSudoSystemCommand) + + fake_dir = Pathname(Dir.mktmpdir) + + fake_file = fake_dir.join("ima_installed_file").tap { |path| FileUtils.touch(path) } + + fake_dir.chmod(0o000) + + pkg.stubs(:pkgutil_bom_specials).returns([]) + pkg.stubs(:pkgutil_bom_files).returns([fake_file]) + pkg.stubs(:pkgutil_bom_dirs).returns([fake_dir]) + pkg.stubs(:forget) + + shutup do + pkg.uninstall + end + + fake_dir.must_be :directory? + fake_file.wont_be :file? + (fake_dir.stat.mode % 0o1000).to_s(8).must_equal "0" + end + end +end diff --git a/Library/Homebrew/cask/test/cask/staged_test.rb b/Library/Homebrew/cask/test/cask/staged_test.rb new file mode 100644 index 0000000000..fe3bf23399 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/staged_test.rb @@ -0,0 +1,18 @@ +require "test_helper" + +# TODO: this test should be named after the corresponding class, once +# that class is abstracted from installer.rb. It makes little sense +# to be invoking bundle_identifier off of the installer instance. +describe "Operations on staged Casks" do + describe "bundle ID" do + it "fetches the bundle ID from a staged cask" do + transmission_cask = Hbc.load("local-transmission") + tr_installer = Hbc::Installer.new(transmission_cask) + + shutup do + tr_installer.install + end + tr_installer.bundle_identifier.must_equal("org.m0k.transmission") + end + end +end diff --git a/Library/Homebrew/cask/test/cask/url_checker_test.rb b/Library/Homebrew/cask/test/cask/url_checker_test.rb new file mode 100644 index 0000000000..cb0e50d575 --- /dev/null +++ b/Library/Homebrew/cask/test/cask/url_checker_test.rb @@ -0,0 +1,31 @@ +require "test_helper" + +describe Hbc::UrlChecker do + describe "request processing" do + it "adds an error if response is empty" do + cask = TestHelper.test_cask + TestHelper.fake_response_for(cask.url, "") + checker = Hbc::UrlChecker.new(cask, TestHelper.fake_fetcher) + checker.run + checker.errors.must_include "timeout while requesting #{cask.url}" + end + + it "properly populates the response code and headers from an http response" do + TestHelper.fake_response_for(TestHelper.test_cask.url, <<-RESPONSE.gsub(%r{^ *}, "")) + HTTP/1.1 200 OK + Content-Type: application/x-apple-diskimage + ETag: "b4208f3e84967be4b078ecaa03fba941" + Content-Length: 23726161 + Last-Modified: Sun, 12 Aug 2012 21:17:21 GMT + RESPONSE + + checker = Hbc::UrlChecker.new(TestHelper.test_cask, TestHelper.fake_fetcher) + checker.run + checker.response_status.must_equal "HTTP/1.1 200 OK" + checker.headers.must_equal("Content-Type" => "application/x-apple-diskimage", + "ETag" => '"b4208f3e84967be4b078ecaa03fba941"', + "Content-Length" => "23726161", + "Last-Modified" => "Sun, 12 Aug 2012 21:17:21 GMT") + end + end +end diff --git a/Library/Homebrew/cask/test/cask_test.rb b/Library/Homebrew/cask/test/cask_test.rb new file mode 100644 index 0000000000..64fc219655 --- /dev/null +++ b/Library/Homebrew/cask/test/cask_test.rb @@ -0,0 +1,71 @@ +require "test_helper" + +describe "Cask" do + hbc_relative_tap_path = "../../Taps/caskroom/homebrew-cask" + describe "load" do + it "returns an instance of the Cask for the given token" do + c = Hbc.load("adium") + c.must_be_kind_of(Hbc::Cask) + c.token.must_equal("adium") + end + + it "returns an instance of the Cask from a specific file location" do + location = File.expand_path(hbc_relative_tap_path + "/Casks/dia.rb") + c = Hbc.load(location) + c.must_be_kind_of(Hbc::Cask) + c.token.must_equal("dia") + end + + it "returns an instance of the Cask from a url" do + url = "file://" + File.expand_path(hbc_relative_tap_path + "/Casks/dia.rb") + c = shutup do + Hbc.load(url) + end + c.must_be_kind_of(Hbc::Cask) + c.token.must_equal("dia") + end + + it "raises an error when failing to download a Cask from a url" do + lambda { + url = "file://" + File.expand_path(hbc_relative_tap_path + "/Casks/notacask.rb") + shutup do + Hbc.load(url) + end + }.must_raise(Hbc::CaskUnavailableError) + end + + it "returns an instance of the Cask from a relative file location" do + c = Hbc.load(hbc_relative_tap_path + "/Casks/bbedit.rb") + c.must_be_kind_of(Hbc::Cask) + c.token.must_equal("bbedit") + end + + it "uses exact match when loading by token" do + Hbc.load("test-opera").token.must_equal("test-opera") + Hbc.load("test-opera-mail").token.must_equal("test-opera-mail") + end + + it "raises an error when attempting to load a Cask that doesn't exist" do + lambda { + Hbc.load("notacask") + }.must_raise(Hbc::CaskUnavailableError) + end + end + + describe "all_tokens" do + it "returns a token for every Cask" do + all_cask_tokens = Hbc.all_tokens + all_cask_tokens.count.must_be :>, 20 + all_cask_tokens.each { |token| token.must_be_kind_of String } + end + end + + describe "metadata" do + it "proposes a versioned metadata directory name for each instance" do + cask_token = "adium" + c = Hbc.load(cask_token) + metadata_path = Hbc.caskroom.join(cask_token, ".metadata", c.version) + c.metadata_versioned_container_path.to_s.must_equal(metadata_path.to_s) + end + end +end diff --git a/Library/Homebrew/cask/test/plist/parser_test.rb b/Library/Homebrew/cask/test/plist/parser_test.rb new file mode 100644 index 0000000000..a73d1f7f58 --- /dev/null +++ b/Library/Homebrew/cask/test/plist/parser_test.rb @@ -0,0 +1,106 @@ +require "test_helper" + +describe Plist do + it "parses some hdiutil output okay" do + hdiutil_output = <<-HDIUTILOUTPUT + + + + + system-entities + + + content-hint + Apple_partition_map + dev-entry + /dev/disk3s1 + potentially-mountable + + unmapped-content-hint + Apple_partition_map + + + content-hint + Apple_partition_scheme + dev-entry + /dev/disk3 + potentially-mountable + + unmapped-content-hint + Apple_partition_scheme + + + content-hint + Apple_HFS + dev-entry + /dev/disk3s2 + mount-point + /private/tmp/dmg.BhfS2g + potentially-mountable + + unmapped-content-hint + Apple_HFS + volume-kind + hfs + + + + + HDIUTILOUTPUT + + parsed = Plist.parse_xml(hdiutil_output) + + parsed.keys.must_equal ["system-entities"] + parsed["system-entities"].length.must_equal 3 + parsed["system-entities"].map { |e| e["dev-entry"] }.must_equal %w[ + /dev/disk3s1 + /dev/disk3 + /dev/disk3s2 + ] + end + + it "can ignore garbage output before xml starts" do + hdiutil_output = <<-HDIUTILOUTPUT +Hello there! I am in no way XML am I?!?! + + That's a little silly... you were expexting XML here! + +What is a parser to do? + +Hopefully explode! + + + + + + system-entities + + + content-hint + Apple_HFS + dev-entry + /dev/disk3s2 + mount-point + /private/tmp/dmg.BhfS2g + potentially-mountable + + unmapped-content-hint + Apple_HFS + volume-kind + hfs + + + + + HDIUTILOUTPUT + + parsed = Plist.parse_xml(hdiutil_output) + + parsed.keys.must_equal ["system-entities"] + parsed["system-entities"].length.must_equal 1 + end + + it "does not choke on empty input" do + Plist.parse_xml("").must_equal {} + end +end diff --git a/Library/Homebrew/cask/test/support/Casks/.rubocop.yml b/Library/Homebrew/cask/test/support/Casks/.rubocop.yml new file mode 120000 index 0000000000..ee5c2b9485 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/.rubocop.yml @@ -0,0 +1 @@ +../../../Casks/.rubocop.yml \ No newline at end of file diff --git a/Library/Homebrew/cask/test/support/Casks/adobe-air-container.rb b/Library/Homebrew/cask/test/support/Casks/adobe-air-container.rb new file mode 100644 index 0000000000..302f15703a --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/adobe-air-container.rb @@ -0,0 +1,9 @@ +test_cask 'adobe-air-container' do + version '1.0.1' + sha256 '9b6e4174afa76f2af50238364fcf87525bc4ed2287acbe62925107ab6cda5c99' + + url TestHelper.local_binary_url('GMDesk-1.01.air') + homepage 'http://robertnyman.com/gmdesk/' + + app 'GMDesk.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/appdir-interpolation.rb b/Library/Homebrew/cask/test/support/Casks/appdir-interpolation.rb new file mode 100644 index 0000000000..6aa3f29c34 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/appdir-interpolation.rb @@ -0,0 +1,9 @@ +test_cask 'appdir-interpolation' do + version '2.61' + sha256 'd26d7481cf1229f879c05e11cbdf440d99db6d6342f26c73d8ba7861b975532f' + + url TestHelper.local_binary_url('transmission-2.61.dmg') + homepage 'http://example.com/appdir-interpolation' + + binary "#{appdir}/some/path" +end diff --git a/Library/Homebrew/cask/test/support/Casks/auto-updates.rb b/Library/Homebrew/cask/test/support/Casks/auto-updates.rb new file mode 100644 index 0000000000..f49060f839 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/auto-updates.rb @@ -0,0 +1,11 @@ +test_cask 'auto-updates' do + version '2.61' + sha256 'd26d7481cf1229f879c05e11cbdf440d99db6d6342f26c73d8ba7861b975532f' + + url TestHelper.local_binary_url('transmission-2.61.dmg') + homepage 'http://example.com/auto-updates' + + auto_updates true + + app 'Transmission.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/bad-checksum.rb b/Library/Homebrew/cask/test/support/Casks/bad-checksum.rb new file mode 100644 index 0000000000..074dae01f3 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/bad-checksum.rb @@ -0,0 +1,9 @@ +test_cask 'bad-checksum' do + version '1.2.3' + sha256 'badbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadbadb' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/basic-cask.rb b/Library/Homebrew/cask/test/support/Casks/basic-cask.rb new file mode 100644 index 0000000000..d3aaa283e9 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/basic-cask.rb @@ -0,0 +1,9 @@ +test_cask 'basic-cask' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url 'http://example.com/TestCask.dmg' + homepage 'http://example.com/' + + app 'TestCask.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/bzipped-asset.rb b/Library/Homebrew/cask/test/support/Casks/bzipped-asset.rb new file mode 100644 index 0000000000..69c31b43c4 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/bzipped-asset.rb @@ -0,0 +1,9 @@ +test_cask 'bzipped-asset' do + version '1.2.3' + sha256 'eaf67b3a62cb9275f96e45d05c70b94bef9ef1dae344083e93eda6b0b388a61c' + + url TestHelper.local_binary_url('bzipped_asset.bz2') + homepage 'http://example.com/bzipped-asset' + + app 'bzipped-asset--1.2.3' +end diff --git a/Library/Homebrew/cask/test/support/Casks/cab-container.rb b/Library/Homebrew/cask/test/support/Casks/cab-container.rb new file mode 100644 index 0000000000..59e25af4e3 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/cab-container.rb @@ -0,0 +1,11 @@ +test_cask 'cab-container' do + version '1.2.3' + sha256 '192d0cf6b727473f9ba0f55cec793ee2a8f7113c5cfe9d49e05a087436c5efe2' + + url TestHelper.local_binary_url('cabcontainer.cab') + homepage 'http://example.com/cab-container' + + depends_on formula: 'cabextract' + + app 'cabcontainer/Application.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/gzipped-asset.rb b/Library/Homebrew/cask/test/support/Casks/gzipped-asset.rb new file mode 100644 index 0000000000..20872922c3 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/gzipped-asset.rb @@ -0,0 +1,9 @@ +test_cask 'gzipped-asset' do + version '1.2.3' + sha256 '832506ade94b3e41ecdf2162654eb75891a0749803229e82b2e0420fd1b9e8d2' + + url TestHelper.local_binary_url('gzipped_asset.gz') + homepage 'http://example.com/gzipped-asset' + + app 'gzipped-asset--1.2.3' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-appcast-multiple.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-appcast-multiple.rb new file mode 100644 index 0000000000..c257b05a12 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-appcast-multiple.rb @@ -0,0 +1,13 @@ +test_cask 'invalid-appcast-multiple' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + appcast 'http://example.com/appcast1.xml', + checkpoint: '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + appcast 'http://example.com/appcast2.xml', + checkpoint: '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + homepage 'http://example.com/invalid-appcast-multiple' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-appcast-url.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-appcast-url.rb new file mode 100644 index 0000000000..c387029648 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-appcast-url.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-appcast-url' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + appcast 1, + checkpoint: '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + homepage 'http://example.com/invalid-appcast-url' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-conflicts-with-key.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-conflicts-with-key.rb new file mode 100644 index 0000000000..c3cdaf90cb --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-conflicts-with-key.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-conflicts-with-key' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-conflicts-with-key' + + conflicts_with no_such_key: 'unar' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-arch-value.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-arch-value.rb new file mode 100644 index 0000000000..68cefaee9a --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-arch-value.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-depends-on-arch-value' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-depends-on-arch-value' + + depends_on arch: :no_such_arch + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-key.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-key.rb new file mode 100644 index 0000000000..99b4387316 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-key.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-depends-on-key' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-depends-on-key' + + depends_on no_such_key: 'unar' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-macos-bad-release.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-macos-bad-release.rb new file mode 100644 index 0000000000..fdf4d6faf7 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-macos-bad-release.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-depends-on-macos-bad-release' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-depends-on-macos-bad-release' + + depends_on macos: :no_such_release + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-macos-conflicting-forms.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-macos-conflicting-forms.rb new file mode 100644 index 0000000000..d00960ecf2 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-macos-conflicting-forms.rb @@ -0,0 +1,12 @@ +test_cask 'invalid-depends-on-macos-conflicting-forms' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-depends-on-macos-conflicting-forms' + + depends_on macos: :yosemite + depends_on macos: '>= :mavericks' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-x11-value.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-x11-value.rb new file mode 100644 index 0000000000..e15e259338 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-depends-on-x11-value.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-depends-on-x11-value' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-depends-on-x11-value' + + depends_on x11: :no_such_value + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-conflicting-keys.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-conflicting-keys.rb new file mode 100644 index 0000000000..a61b7b045d --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-conflicting-keys.rb @@ -0,0 +1,12 @@ +test_cask 'invalid-gpg-conflicting-keys' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-gpg-conflicting-keys' + gpg 'http://example.com/gpg-signature.asc', + key_id: '01234567', + key_url: 'http://example.com/gpg-key-url' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-key-id.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-key-id.rb new file mode 100644 index 0000000000..ba6761311e --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-key-id.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-gpg-key-id' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-gpg-key-id' + gpg 'http://example.com/gpg-signature.asc', + key_id: '012' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-key-url.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-key-url.rb new file mode 100644 index 0000000000..7e4fc33c1f --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-key-url.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-gpg-key-url' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-gpg-key-url' + gpg 'http://example.com/gpg-signature.asc', + key_url: 1 + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-missing-key.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-missing-key.rb new file mode 100644 index 0000000000..3392286263 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-missing-key.rb @@ -0,0 +1,10 @@ +test_cask 'invalid-gpg-missing-key' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-gpg-missing-keys' + gpg 'http://example.com/gpg-signature.asc' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-multiple-stanzas.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-multiple-stanzas.rb new file mode 100644 index 0000000000..94125f9d9a --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-multiple-stanzas.rb @@ -0,0 +1,13 @@ +test_cask 'invalid-gpg-multiple-stanzas' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-gpg-multiple-stanzas' + gpg 'http://example.com/gpg-signature.asc', + key_id: '01234567' + gpg 'http://example.com/gpg-signature.asc', + key_id: '01234567' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-parameter.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-parameter.rb new file mode 100644 index 0000000000..0e08f0c180 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-parameter.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-gpg-parameter' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-gpg-type' + gpg 'http://example.com/gpg-signature.asc', + no_such_parameter: :value + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-signature-url.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-signature-url.rb new file mode 100644 index 0000000000..f1cdfcb630 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-signature-url.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-gpg-signature-url' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-gpg-signature-url' + gpg 1, + key_id: '01234567' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-type.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-type.rb new file mode 100644 index 0000000000..bfb67b3f94 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-gpg-type.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-gpg-type' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/invalid-gpg-type' + gpg 'http://example.com/gpg-signature.asc', + no_such_parameter: :value + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-header-format.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-header-format.rb new file mode 100644 index 0000000000..90ede3b2f5 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-header-format.rb @@ -0,0 +1,9 @@ +test_cask => 'invalid-header-format' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-header-token-mismatch.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-header-token-mismatch.rb new file mode 100644 index 0000000000..a69b0d7524 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-header-token-mismatch.rb @@ -0,0 +1,9 @@ +test_cask 'invalid-header-token-mismatch-this-text-does-not-belong' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-header-version.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-header-version.rb new file mode 100644 index 0000000000..d49b834679 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-header-version.rb @@ -0,0 +1,9 @@ +test_cask 'invalid-header-version' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-license-multiple.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-license-multiple.rb new file mode 100644 index 0000000000..5144152f50 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-license-multiple.rb @@ -0,0 +1,11 @@ +test_cask 'invalid-license-multiple' do + version '2.61' + sha256 'd26d7481cf1229f879c05e11cbdf440d99db6d6342f26c73d8ba7861b975532f' + + url TestHelper.local_binary_url('transmission-2.61.dmg') + homepage 'http://example.com/invalid-license-multiple' + license :gpl + license :gpl + + app 'Transmission.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-license-value.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-license-value.rb new file mode 100644 index 0000000000..5cb1b08eaf --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-license-value.rb @@ -0,0 +1,10 @@ +test_cask 'invalid-license-value' do + version '2.61' + sha256 'd26d7481cf1229f879c05e11cbdf440d99db6d6342f26c73d8ba7861b975532f' + + url TestHelper.local_binary_url('transmission-2.61.dmg') + homepage 'http://example.com/invalid-license-value' + license :no_such_license + + app 'Transmission.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-stage-only-conflict.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-stage-only-conflict.rb new file mode 100644 index 0000000000..6efd64b22c --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-stage-only-conflict.rb @@ -0,0 +1,10 @@ +test_cask 'invalid-stage-only-conflict' do + version '2.61' + sha256 'd26d7481cf1229f879c05e11cbdf440d99db6d6342f26c73d8ba7861b975532f' + + url TestHelper.local_binary_url('transmission-2.61.dmg') + homepage 'http://example.com/invalid-stage-only-conflict' + + app 'Transmission.app' + stage_only true +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-two-homepage.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-two-homepage.rb new file mode 100644 index 0000000000..ee90fa81a0 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-two-homepage.rb @@ -0,0 +1,10 @@ +test_cask 'invalid-two-homepage' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + homepage 'http://www.example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-two-url.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-two-url.rb new file mode 100644 index 0000000000..7a4dce7aef --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-two-url.rb @@ -0,0 +1,10 @@ +test_cask 'invalid-two-url' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + url 'http://example.com/caffeine.zip' + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/invalid/invalid-two-version.rb b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-two-version.rb new file mode 100644 index 0000000000..a3c47c9072 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/invalid/invalid-two-version.rb @@ -0,0 +1,10 @@ +test_cask 'invalid-two-version' do + version '1.2.3' + version '2.0' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/local-caffeine.rb b/Library/Homebrew/cask/test/support/Casks/local-caffeine.rb new file mode 100644 index 0000000000..fb217cc20f --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/local-caffeine.rb @@ -0,0 +1,9 @@ +test_cask 'local-caffeine' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/local-transmission.rb b/Library/Homebrew/cask/test/support/Casks/local-transmission.rb new file mode 100644 index 0000000000..e0aa719a56 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/local-transmission.rb @@ -0,0 +1,9 @@ +test_cask 'local-transmission' do + version '2.61' + sha256 'd26d7481cf1229f879c05e11cbdf440d99db6d6342f26c73d8ba7861b975532f' + + url TestHelper.local_binary_url('transmission-2.61.dmg') + homepage 'http://example.com/local-transmission' + + app 'Transmission.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/lzma-asset.rb b/Library/Homebrew/cask/test/support/Casks/lzma-asset.rb new file mode 100644 index 0000000000..f69bc7974e --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/lzma-asset.rb @@ -0,0 +1,11 @@ +test_cask 'lzma-asset' do + version '1.2.3' + sha256 '9d7edb32d02ab9bd9749a5bde8756595ea4cfcb1da02ca11c30fb591d4c1ed85' + + url TestHelper.local_binary_url('lzma-asset.lzma') + homepage 'http://example.com/xzipped-asset' + + depends_on formula: 'lzma' + + app 'lzma-asset--1.2.3' +end diff --git a/Library/Homebrew/cask/test/support/Casks/missing-checksum.rb b/Library/Homebrew/cask/test/support/Casks/missing-checksum.rb new file mode 100644 index 0000000000..3187f6fd23 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/missing-checksum.rb @@ -0,0 +1,8 @@ +test_cask 'missing-checksum' do + version '1.2.3' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/naked-executable.rb b/Library/Homebrew/cask/test/support/Casks/naked-executable.rb new file mode 100644 index 0000000000..03e89e97b1 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/naked-executable.rb @@ -0,0 +1,9 @@ +test_cask 'naked-executable' do + version '1.2.3' + sha256 '306c6ca7407560340797866e077e053627ad409277d1b9da58106fce4cf717cb' + + url TestHelper.local_binary_url('naked_executable') + homepage 'http://example.com/naked-executable' + + container type: :naked +end diff --git a/Library/Homebrew/cask/test/support/Casks/naked-pkg.rb b/Library/Homebrew/cask/test/support/Casks/naked-pkg.rb new file mode 100644 index 0000000000..4650ce55df --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/naked-pkg.rb @@ -0,0 +1,7 @@ +test_cask 'naked-pkg' do + version '1.2.3' + sha256 '611c50c8a2a2098951d2cd0fd54787ed81b92cd97b4b08bd7cba17f1e1d8e40b' + + url TestHelper.local_binary_url('Naked.pkg') + homepage 'http://example.com/naked-pkg' +end diff --git a/Library/Homebrew/cask/test/support/Casks/nested-app.rb b/Library/Homebrew/cask/test/support/Casks/nested-app.rb new file mode 100644 index 0000000000..77bd04127d --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/nested-app.rb @@ -0,0 +1,11 @@ +test_cask 'nested-app' do + version '1.2.3' + sha256 '1866dfa833b123bb8fe7fa7185ebf24d28d300d0643d75798bc23730af734216' + + url TestHelper.local_binary_url('NestedApp.dmg.zip') + homepage 'http://example.com/nested-app' + + container nested: 'NestedApp.dmg' + + app 'MyNestedApp.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/no-checksum.rb b/Library/Homebrew/cask/test/support/Casks/no-checksum.rb new file mode 100644 index 0000000000..dbe62b02b6 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/no-checksum.rb @@ -0,0 +1,9 @@ +test_cask 'no-checksum' do + version '1.2.3' + sha256 :no_check + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/no-dsl-version.rb b/Library/Homebrew/cask/test/support/Casks/no-dsl-version.rb new file mode 100644 index 0000000000..ee557b1e40 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/no-dsl-version.rb @@ -0,0 +1,9 @@ +test_cask 'no-dsl-version' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url 'http://example.com/TestCask.dmg' + homepage 'http://example.com/' + + app 'TestCask.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/rar-container.rb b/Library/Homebrew/cask/test/support/Casks/rar-container.rb new file mode 100644 index 0000000000..60c525b601 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/rar-container.rb @@ -0,0 +1,11 @@ +test_cask 'rar-container' do + version '1.2.3' + sha256 '35fb13fb13e6aefc38b60486627eff6b6b55b2f99f64bf47938530c6cf9e0a0f' + + url TestHelper.local_binary_url('rarcontainer.rar') + homepage 'http://example.com/rar-container' + + depends_on formula: 'unar' + + app 'rarcontainer/Application.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/sevenzip-container.rb b/Library/Homebrew/cask/test/support/Casks/sevenzip-container.rb new file mode 100644 index 0000000000..7f8cfdb4e8 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/sevenzip-container.rb @@ -0,0 +1,11 @@ +test_cask 'sevenzip-container' do + version '1.2.3' + sha256 '1550701e7848fcb94f5b0085cca527083a8662ddeb8c0a7bc5af6bd145797cc1' + + url TestHelper.local_binary_url('sevenzipcontainer.7z') + homepage 'http://example.com/sevenzip-container' + + depends_on formula: 'unar' + + app 'sevenzipcontainer/Application.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/stage-only.rb b/Library/Homebrew/cask/test/support/Casks/stage-only.rb new file mode 100644 index 0000000000..25fa7460c3 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/stage-only.rb @@ -0,0 +1,9 @@ +test_cask 'stage-only' do + version '2.61' + sha256 'd26d7481cf1229f879c05e11cbdf440d99db6d6342f26c73d8ba7861b975532f' + + url TestHelper.local_binary_url('transmission-2.61.dmg') + homepage 'http://example.com/stage-only' + + stage_only true +end diff --git a/Library/Homebrew/cask/test/support/Casks/stuffit-container.rb b/Library/Homebrew/cask/test/support/Casks/stuffit-container.rb new file mode 100644 index 0000000000..a42971ff50 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/stuffit-container.rb @@ -0,0 +1,11 @@ +test_cask 'stuffit-container' do + version '1.2.3' + sha256 '892b6d49a98c546381d41dec9b0bbc04267ac008d72b99755968d357099993b7' + + url TestHelper.local_binary_url('sheldonmac.sit') + homepage 'http://www.tobias-jung.de/seekingprofont/' + + depends_on formula: 'unar' + + artifact 'sheldonmac/v1.0', target: "#{Hbc.appdir}/sheldonmac/v1.0" +end diff --git a/Library/Homebrew/cask/test/support/Casks/tarball.rb b/Library/Homebrew/cask/test/support/Casks/tarball.rb new file mode 100644 index 0000000000..3134a2f943 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/tarball.rb @@ -0,0 +1,9 @@ +test_cask 'tarball' do + version '1.2.3' + sha256 :no_check + + url TestHelper.local_binary_url('tarball.tgz') + homepage 'http://example.com/tarball' + + app 'Tarball.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/test-opera-mail.rb b/Library/Homebrew/cask/test/support/Casks/test-opera-mail.rb new file mode 100644 index 0000000000..afc8387c7c --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/test-opera-mail.rb @@ -0,0 +1,9 @@ +test_cask 'test-opera-mail' do + version '1.0' + sha256 'afd192e308f8ea8ddb3d426fd6663d97078570417ee78b8e1fa15f515ae3d677' + + url 'http://get-ash-1.opera.com/pub/opera/mail/1.0/mac/Opera-Mail-1.0-1040.i386.dmg' + homepage 'http://www.opera.com/computer/mail' + + app 'Opera Mail.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/test-opera.rb b/Library/Homebrew/cask/test/support/Casks/test-opera.rb new file mode 100644 index 0000000000..ec32828f7d --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/test-opera.rb @@ -0,0 +1,9 @@ +test_cask 'test-opera' do + version '19.0.1326.47' + sha256 '7b91f20ab754f7b3fef8dc346e0393917e11676b74c8f577408841619f76040a' + + url 'http://get.geo.opera.com/pub/opera/desktop/19.0.1326.47/mac/Opera_19.0.1326.47_Setup.dmg' + homepage 'http://www.opera.com/' + + app 'Opera.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-accessibility-access.rb b/Library/Homebrew/cask/test/support/Casks/with-accessibility-access.rb new file mode 100644 index 0000000000..1aa7bc8f7a --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-accessibility-access.rb @@ -0,0 +1,11 @@ +test_cask 'with-accessibility-access' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url 'http://example.com/TestCask.dmg' + homepage 'http://example.com/' + + accessibility_access true + + app 'TestCask.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-alt-target.rb b/Library/Homebrew/cask/test/support/Casks/with-alt-target.rb new file mode 100644 index 0000000000..6d6456d86c --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-alt-target.rb @@ -0,0 +1,9 @@ +test_cask 'with-alt-target' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app', target: 'AnotherName.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-appcast.rb b/Library/Homebrew/cask/test/support/Casks/with-appcast.rb new file mode 100644 index 0000000000..acfb1565fb --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-appcast.rb @@ -0,0 +1,11 @@ +test_cask 'with-appcast' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + appcast 'http://example.com/appcast.xml', + checkpoint: '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + homepage 'http://example.com/with-appcast' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-caveats.rb b/Library/Homebrew/cask/test/support/Casks/with-caveats.rb new file mode 100644 index 0000000000..4412ceb3a2 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-caveats.rb @@ -0,0 +1,23 @@ +test_cask 'with-caveats' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' + + # simple string is evaluated at compile-time + caveats <<-EOS.undent + Here are some things you might want to know. + EOS + # do block is evaluated at install-time + caveats do + "Cask token: #{token}" + end + # a do block may print and use a DSL + caveats do + puts 'Custom text via puts followed by DSL-generated text:' + path_environment_variable('/custom/path/bin') + end +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-conditional-caveats.rb b/Library/Homebrew/cask/test/support/Casks/with-conditional-caveats.rb new file mode 100644 index 0000000000..2690649c3e --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-conditional-caveats.rb @@ -0,0 +1,14 @@ +test_cask 'with-conditional-caveats' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' + + # a do block may print and use a DSL + caveats do + puts 'This caveat is conditional' if false # rubocop:disable Lint/LiteralInCondition + end +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-conflicts-with.rb b/Library/Homebrew/cask/test/support/Casks/with-conflicts-with.rb new file mode 100644 index 0000000000..789c9839ab --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-conflicts-with.rb @@ -0,0 +1,11 @@ +test_cask 'with-conflicts-with' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-conflicts-with' + + conflicts_with formula: 'unar' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-arch-failure.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-arch-failure.rb new file mode 100644 index 0000000000..a257d7bbbf --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-arch-failure.rb @@ -0,0 +1,12 @@ +test_cask 'with-depends-on-arch-failure' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-arch-failure' + + # guarantee mismatched hardware + depends_on arch: Hardware::CPU.intel? ? :ppc : :intel + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-arch.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-arch.rb new file mode 100644 index 0000000000..9fc5baf1ad --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-arch.rb @@ -0,0 +1,12 @@ +test_cask 'with-depends-on-arch' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-arch' + + # covers all known hardware; always succeeds + depends_on arch: [:ppc, :intel] + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask-cyclic-helper.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask-cyclic-helper.rb new file mode 100644 index 0000000000..7352895917 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask-cyclic-helper.rb @@ -0,0 +1,11 @@ +test_cask 'with-depends-on-cask-cyclic-helper' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-cask-cyclic-helper' + + depends_on cask: 'with-depends-on-cask-cyclic' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask-cyclic.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask-cyclic.rb new file mode 100644 index 0000000000..776929e835 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask-cyclic.rb @@ -0,0 +1,12 @@ +test_cask 'with-depends-on-cask-cyclic' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-cask-cyclic' + + depends_on cask: 'local-caffeine' + depends_on cask: 'with-depends-on-cask-cyclic-helper' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask-multiple.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask-multiple.rb new file mode 100644 index 0000000000..f08e73ac44 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask-multiple.rb @@ -0,0 +1,12 @@ +test_cask 'with-depends-on-cask-multiple' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-cask-multiple' + + depends_on cask: 'local-caffeine' + depends_on cask: 'local-transmission' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask.rb new file mode 100644 index 0000000000..013da2379e --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-cask.rb @@ -0,0 +1,11 @@ +test_cask 'with-depends-on-cask' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-cask' + + depends_on cask: 'local-transmission' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-formula-multiple.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-formula-multiple.rb new file mode 100644 index 0000000000..bdc4e1f8b8 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-formula-multiple.rb @@ -0,0 +1,12 @@ +test_cask 'with-depends-on-formula-multiple' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-formula-multiple' + + depends_on formula: 'unar' + depends_on formula: 'fileutils' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-formula.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-formula.rb new file mode 100644 index 0000000000..3d365a1876 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-formula.rb @@ -0,0 +1,11 @@ +test_cask 'with-depends-on-formula' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-formula' + + depends_on formula: 'unar' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-array.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-array.rb new file mode 100644 index 0000000000..4b3b8320e3 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-array.rb @@ -0,0 +1,12 @@ +test_cask 'with-depends-on-macos-array' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-macos-array' + + # since all OS releases are included, this should always pass + depends_on macos: ['10.4', '10.5', '10.6', '10.7', '10.8', '10.9', '10.10', MacOS.version.to_s] + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-comparison.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-comparison.rb new file mode 100644 index 0000000000..448e44a03a --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-comparison.rb @@ -0,0 +1,11 @@ +test_cask 'with-depends-on-macos-comparison' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-macos-comparison' + + depends_on macos: '>= 10.4' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-failure.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-failure.rb new file mode 100644 index 0000000000..8e7a9a1d63 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-failure.rb @@ -0,0 +1,12 @@ +test_cask 'with-depends-on-macos-failure' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-macos-failure' + + # guarantee a mismatched release + depends_on macos: MacOS.version.to_s == '10.4' ? '10.5' : '10.4' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-string.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-string.rb new file mode 100644 index 0000000000..1dab7654c7 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-string.rb @@ -0,0 +1,11 @@ +test_cask 'with-depends-on-macos-string' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-macos-string' + + depends_on macos: MacOS.version.to_s + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-symbol.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-symbol.rb new file mode 100644 index 0000000000..00a99c525f --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-macos-symbol.rb @@ -0,0 +1,11 @@ +test_cask 'with-depends-on-macos-symbol' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-macos-symbol' + + depends_on macos: MacOS.version.to_sym + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-x11-false.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-x11-false.rb new file mode 100644 index 0000000000..00ea1cef93 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-x11-false.rb @@ -0,0 +1,11 @@ +test_cask 'with-depends-on-x11-false' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-x11-false' + + depends_on x11: false + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-depends-on-x11.rb b/Library/Homebrew/cask/test/support/Casks/with-depends-on-x11.rb new file mode 100644 index 0000000000..2345473340 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-depends-on-x11.rb @@ -0,0 +1,11 @@ +test_cask 'with-depends-on-x11' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-depends-on-x11' + + depends_on x11: true + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-generic-artifact-no-target.rb b/Library/Homebrew/cask/test/support/Casks/with-generic-artifact-no-target.rb new file mode 100644 index 0000000000..5e6eb51b80 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-generic-artifact-no-target.rb @@ -0,0 +1,9 @@ +test_cask 'with-generic-artifact-no-target' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-generic-artifact' + + artifact 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-generic-artifact.rb b/Library/Homebrew/cask/test/support/Casks/with-generic-artifact.rb new file mode 100644 index 0000000000..87bf43cba0 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-generic-artifact.rb @@ -0,0 +1,9 @@ +test_cask 'with-generic-artifact' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-generic-artifact' + + artifact 'Caffeine.app', target: "#{Hbc.appdir}/Caffeine.app" +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-gpg-key-url.rb b/Library/Homebrew/cask/test/support/Casks/with-gpg-key-url.rb new file mode 100644 index 0000000000..68fa8123af --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-gpg-key-url.rb @@ -0,0 +1,11 @@ +test_cask 'with-gpg-key-url' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-gpg-key-url' + gpg 'http://example.com/gpg-signature.asc', + key_url: 'http://example.com/gpg-key-url' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-gpg.rb b/Library/Homebrew/cask/test/support/Casks/with-gpg.rb new file mode 100644 index 0000000000..84c29fe658 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-gpg.rb @@ -0,0 +1,11 @@ +test_cask 'with-gpg' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-gpg' + gpg 'http://example.com/gpg-signature.asc', + key_id: '01234567' + + app 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-installable.rb b/Library/Homebrew/cask/test/support/Casks/with-installable.rb new file mode 100644 index 0000000000..b32273a6c2 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-installable.rb @@ -0,0 +1,20 @@ +test_cask 'with-installable' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + uninstall script: { executable: 'MyFancyPkg/FancyUninstaller.tool', args: %w[--please] }, + quit: 'my.fancy.package.app', + login_item: 'Fancy', + delete: [ + '/permissible/absolute/path', + '~/permissible/path/with/tilde', + 'impermissible/relative/path', + '/another/impermissible/../relative/path', + ], + rmdir: TestHelper.local_binary_path('empty_directory') +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-installer-manual.rb b/Library/Homebrew/cask/test/support/Casks/with-installer-manual.rb new file mode 100644 index 0000000000..6291d7f880 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-installer-manual.rb @@ -0,0 +1,9 @@ +test_cask 'with-installer-manual' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + installer manual: 'Caffeine.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-installer-script.rb b/Library/Homebrew/cask/test/support/Casks/with-installer-script.rb new file mode 100644 index 0000000000..3014a39456 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-installer-script.rb @@ -0,0 +1,15 @@ +test_cask 'with-installer-script' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/with-install-script' + + installer script: '/usr/bin/true', + args: ['--flag'] + # acceptable alternate form + installer script: { + executable: '/usr/bin/false', + args: ['--flag'], + } +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-license.rb b/Library/Homebrew/cask/test/support/Casks/with-license.rb new file mode 100644 index 0000000000..ed2572cf2a --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-license.rb @@ -0,0 +1,10 @@ +test_cask 'with-license' do + version '2.61' + sha256 'd26d7481cf1229f879c05e11cbdf440d99db6d6342f26c73d8ba7861b975532f' + + url TestHelper.local_binary_url('transmission-2.61.dmg') + homepage 'http://example.com/with-license' + license :gpl + + app 'Transmission.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-macosx-dir.rb b/Library/Homebrew/cask/test/support/Casks/with-macosx-dir.rb new file mode 100644 index 0000000000..24c4ec4dd8 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-macosx-dir.rb @@ -0,0 +1,9 @@ +test_cask 'with-macosx-dir' do + version '1.2.3' + sha256 '5633c3a0f2e572cbf021507dec78c50998b398c343232bdfc7e26221d0a5db4d' + + url TestHelper.local_binary_url('MyFancyApp.zip') + homepage 'http://example.com/MyFancyApp' + + app 'MyFancyApp/MyFancyApp.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-pkgutil-zap.rb b/Library/Homebrew/cask/test/support/Casks/with-pkgutil-zap.rb new file mode 100644 index 0000000000..6440285a01 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-pkgutil-zap.rb @@ -0,0 +1,13 @@ +test_cask 'with-pkgutil-zap' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'Fancy.pkg' + + zap pkgutil: 'my.fancy.package.*', + kext: 'my.fancy.package.kernelextension', + launchctl: 'my.fancy.package.service' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-suite.rb b/Library/Homebrew/cask/test/support/Casks/with-suite.rb new file mode 100644 index 0000000000..594276574d --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-suite.rb @@ -0,0 +1,11 @@ +test_cask 'with-suite' do + version '1.2.3' + sha256 'd1302a0dc25aff72ad395ed01a830468b92253ffd28269574f3ac0b5eb8aad54' + + url TestHelper.local_binary_url('caffeine_suite.zip') + name 'Caffeine' + homepage 'http://example.com/with-suite' + license :unknown # TODO: change license and remove this comment; ':unknown' is a machine-generated placeholder + + suite 'caffeine_suite' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-two-apps-correct.rb b/Library/Homebrew/cask/test/support/Casks/with-two-apps-correct.rb new file mode 100644 index 0000000000..a0b142528a --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-two-apps-correct.rb @@ -0,0 +1,10 @@ +test_cask 'with-two-apps-correct' do + version '1.2.3' + sha256 'c0c79dce9511c80603328013dbbcb80b859cc8b9190660b6832b5f0e60d74c82' + + url TestHelper.local_binary_url('2_app_caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app' + app 'Caffeine-2.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-two-apps-incorrect.rb b/Library/Homebrew/cask/test/support/Casks/with-two-apps-incorrect.rb new file mode 100644 index 0000000000..0ae37a79ce --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-two-apps-incorrect.rb @@ -0,0 +1,9 @@ +test_cask 'with-two-apps-incorrect' do + version '1.2.3' + sha256 '9203c30951f9aab41ac294bbeb1dcef7bed401ff0b353dcb34d68af32ea51853' + + url TestHelper.local_binary_url('caffeine.zip') + homepage 'http://example.com/local-caffeine' + + app 'Caffeine.app', 'Caffeine.app/Contents/MacOS/Caffeine' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-two-apps-subdir.rb b/Library/Homebrew/cask/test/support/Casks/with-two-apps-subdir.rb new file mode 100644 index 0000000000..61adefab67 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-two-apps-subdir.rb @@ -0,0 +1,10 @@ +test_cask 'with-two-apps-subdir' do + version '1.2.3' + sha256 '03edce6992a6095e120dcfadf7049158589ae6f0548c145ed1b1c6f2883f6dca' + + url TestHelper.local_binary_url('2_app_subdir.zip') + homepage 'http://example.com/local-caffeine' + + app 'subdir/Caffeine.app' + app 'subdir/Caffeine-2.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-delete.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-delete.rb new file mode 100644 index 0000000000..c85b3bd0f9 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-delete.rb @@ -0,0 +1,16 @@ +test_cask 'with-uninstall-delete' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'Fancy.pkg' + + uninstall delete: [ + '/permissible/absolute/path', + '~/permissible/path/with/tilde', + 'impermissible/relative/path', + '/another/impermissible/../relative/path', + ] +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-early-script.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-early-script.rb new file mode 100644 index 0000000000..6fe3842e21 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-early-script.rb @@ -0,0 +1,11 @@ +test_cask 'with-uninstall-early-script' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + uninstall early_script: { executable: 'MyFancyPkg/FancyUninstaller.tool', args: %w[--please] } +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-kext.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-kext.rb new file mode 100644 index 0000000000..682ce689e7 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-kext.rb @@ -0,0 +1,11 @@ +test_cask 'with-uninstall-kext' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'Fancy.pkg' + + uninstall kext: 'my.fancy.package.kernelextension' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-launchctl.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-launchctl.rb new file mode 100644 index 0000000000..0e601b41e9 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-launchctl.rb @@ -0,0 +1,11 @@ +test_cask 'with-uninstall-launchctl' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyApp.zip') + homepage 'http://example.com/fancy' + + app 'Fancy.app' + + uninstall launchctl: 'my.fancy.package.service' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-login-item.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-login-item.rb new file mode 100644 index 0000000000..d977b31e33 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-login-item.rb @@ -0,0 +1,11 @@ +test_cask 'with-uninstall-login-item' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + uninstall login_item: 'Fancy' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-pkgutil.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-pkgutil.rb new file mode 100644 index 0000000000..773e1e43b6 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-pkgutil.rb @@ -0,0 +1,11 @@ +test_cask 'with-uninstall-pkgutil' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'Fancy.pkg' + + uninstall pkgutil: 'my.fancy.package.*' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-quit.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-quit.rb new file mode 100644 index 0000000000..88e063c627 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-quit.rb @@ -0,0 +1,11 @@ +test_cask 'with-uninstall-quit' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + uninstall quit: 'my.fancy.package.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-rmdir.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-rmdir.rb new file mode 100644 index 0000000000..ce1c665186 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-rmdir.rb @@ -0,0 +1,11 @@ +test_cask 'with-uninstall-rmdir' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + uninstall rmdir: TestHelper.local_binary_path('empty_directory') +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-script.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-script.rb new file mode 100644 index 0000000000..975a144422 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-script.rb @@ -0,0 +1,11 @@ +test_cask 'with-uninstall-script' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + uninstall script: { executable: 'MyFancyPkg/FancyUninstaller.tool', args: %w[--please] } +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-signal.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-signal.rb new file mode 100644 index 0000000000..8087681739 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-signal.rb @@ -0,0 +1,14 @@ +test_cask 'with-uninstall-signal' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + uninstall signal: [ + ['TERM', 'my.fancy.package.app'], + ['KILL', 'my.fancy.package.app'], + ] +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-uninstall-trash.rb b/Library/Homebrew/cask/test/support/Casks/with-uninstall-trash.rb new file mode 100644 index 0000000000..16f174c2d6 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-uninstall-trash.rb @@ -0,0 +1,16 @@ +test_cask 'with-uninstall-trash' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'Fancy.pkg' + + uninstall trash: [ + '/permissible/absolute/path', + '~/permissible/path/with/tilde', + 'impermissible/relative/path', + '/another/impermissible/../relative/path', + ] +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-delete.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-delete.rb new file mode 100644 index 0000000000..77d50ffbaa --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-delete.rb @@ -0,0 +1,16 @@ +test_cask 'with-zap-delete' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'Fancy.pkg' + + zap delete: [ + '/permissible/absolute/path', + '~/permissible/path/with/tilde', + 'impermissible/relative/path', + '/another/impermissible/../relative/path', + ] +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-early-script.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-early-script.rb new file mode 100644 index 0000000000..299c4d135e --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-early-script.rb @@ -0,0 +1,11 @@ +test_cask 'with-zap-early-script' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + zap early_script: { executable: 'MyFancyPkg/FancyUninstaller.tool', args: %w[--please] } +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-kext.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-kext.rb new file mode 100644 index 0000000000..d8809b4097 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-kext.rb @@ -0,0 +1,11 @@ +test_cask 'with-zap-kext' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'Fancy.pkg' + + zap kext: 'my.fancy.package.kernelextension' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-launchctl.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-launchctl.rb new file mode 100644 index 0000000000..321e17a789 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-launchctl.rb @@ -0,0 +1,11 @@ +test_cask 'with-zap-launchctl' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyApp.zip') + homepage 'http://example.com/fancy' + + app 'Fancy.app' + + zap launchctl: 'my.fancy.package.service' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-login-item.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-login-item.rb new file mode 100644 index 0000000000..a7af6ce63f --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-login-item.rb @@ -0,0 +1,11 @@ +test_cask 'with-zap-login-item' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + zap login_item: 'Fancy' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-pkgutil.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-pkgutil.rb new file mode 100644 index 0000000000..ded6508cd1 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-pkgutil.rb @@ -0,0 +1,11 @@ +test_cask 'with-zap-pkgutil' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'Fancy.pkg' + + zap pkgutil: 'my.fancy.package.*' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-quit.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-quit.rb new file mode 100644 index 0000000000..0b21221c86 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-quit.rb @@ -0,0 +1,11 @@ +test_cask 'with-zap-quit' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + zap quit: 'my.fancy.package.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-rmdir.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-rmdir.rb new file mode 100644 index 0000000000..f3f7999195 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-rmdir.rb @@ -0,0 +1,11 @@ +test_cask 'with-zap-rmdir' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + zap rmdir: TestHelper.local_binary_path('empty_directory') +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-script.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-script.rb new file mode 100644 index 0000000000..9e8ae0d121 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-script.rb @@ -0,0 +1,11 @@ +test_cask 'with-zap-script' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + zap script: { executable: 'MyFancyPkg/FancyUninstaller.tool', args: %w[--please] } +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-signal.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-signal.rb new file mode 100644 index 0000000000..7338b67b0c --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-signal.rb @@ -0,0 +1,14 @@ +test_cask 'with-zap-signal' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + zap signal: [ + ['TERM', 'my.fancy.package.app'], + ['KILL', 'my.fancy.package.app'], + ] +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap-trash.rb b/Library/Homebrew/cask/test/support/Casks/with-zap-trash.rb new file mode 100644 index 0000000000..7295b7a235 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap-trash.rb @@ -0,0 +1,16 @@ +test_cask 'with-zap-trash' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'Fancy.pkg' + + zap trash: [ + '/permissible/absolute/path', + '~/permissible/path/with/tilde', + 'impermissible/relative/path', + '/another/impermissible/../relative/path', + ] +end diff --git a/Library/Homebrew/cask/test/support/Casks/with-zap.rb b/Library/Homebrew/cask/test/support/Casks/with-zap.rb new file mode 100644 index 0000000000..752aca019b --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/with-zap.rb @@ -0,0 +1,19 @@ +test_cask 'with-zap' do + version '1.2.3' + sha256 '8c62a2b791cf5f0da6066a0a4b6e85f62949cd60975da062df44adf887f4370b' + + url TestHelper.local_binary_url('MyFancyPkg.zip') + homepage 'http://example.com/fancy-pkg' + + pkg 'MyFancyPkg/Fancy.pkg' + + uninstall quit: 'my.fancy.package.app.from.uninstall' + + zap script: { + executable: 'MyFancyPkg/FancyUninstaller.tool', + args: %w[--please], + }, + quit: 'my.fancy.package.app', + login_item: 'Fancy', + delete: '~/Library/Preferences/my.fancy.app.plist' +end diff --git a/Library/Homebrew/cask/test/support/Casks/xar-container.rb b/Library/Homebrew/cask/test/support/Casks/xar-container.rb new file mode 100644 index 0000000000..158c4f7172 --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/xar-container.rb @@ -0,0 +1,9 @@ +test_cask 'xar-container' do + version '1.2.3' + sha256 '1418752ac49e859f88590db245015cb2f8b459f619e0c50fd6ff87b902c72ee1' + + url TestHelper.local_binary_url('xarcontainer.xar') + homepage 'http://example.com/xar-container' + + app 'xarcontainer/Application.app' +end diff --git a/Library/Homebrew/cask/test/support/Casks/xzipped-asset.rb b/Library/Homebrew/cask/test/support/Casks/xzipped-asset.rb new file mode 100644 index 0000000000..badb252eef --- /dev/null +++ b/Library/Homebrew/cask/test/support/Casks/xzipped-asset.rb @@ -0,0 +1,11 @@ +test_cask 'xzipped-asset' do + version '1.2.3' + sha256 '839263f474edde1d54a9101606e6f0dc9d963acc93f6dcc5af8d10ebc3187c02' + + url TestHelper.local_binary_url('xzipped-asset.xz') + homepage 'http://example.com/xzipped-asset' + + depends_on formula: 'xz' + + app 'xzipped-asset--1.2.3' +end diff --git a/Library/Homebrew/cask/test/support/binaries/2_app_caffeine.zip b/Library/Homebrew/cask/test/support/binaries/2_app_caffeine.zip new file mode 100755 index 0000000000..a678f7c11b Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/2_app_caffeine.zip differ diff --git a/Library/Homebrew/cask/test/support/binaries/2_app_subdir.zip b/Library/Homebrew/cask/test/support/binaries/2_app_subdir.zip new file mode 100755 index 0000000000..27637a731d Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/2_app_subdir.zip differ diff --git a/Library/Homebrew/cask/test/support/binaries/GMDesk-1.01.air b/Library/Homebrew/cask/test/support/binaries/GMDesk-1.01.air new file mode 100644 index 0000000000..215cd2b8b3 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/GMDesk-1.01.air differ diff --git a/Library/Homebrew/cask/test/support/binaries/MyFancyApp.zip b/Library/Homebrew/cask/test/support/binaries/MyFancyApp.zip new file mode 100644 index 0000000000..ba09ee681a Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/MyFancyApp.zip differ diff --git a/Library/Homebrew/cask/test/support/binaries/MyFancyPkg.zip b/Library/Homebrew/cask/test/support/binaries/MyFancyPkg.zip new file mode 100644 index 0000000000..ec4f97f071 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/MyFancyPkg.zip differ diff --git a/Library/Homebrew/cask/test/support/binaries/Naked.pkg b/Library/Homebrew/cask/test/support/binaries/Naked.pkg new file mode 100644 index 0000000000..274f27e7a1 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/Naked.pkg differ diff --git a/Library/Homebrew/cask/test/support/binaries/NestedApp.dmg.zip b/Library/Homebrew/cask/test/support/binaries/NestedApp.dmg.zip new file mode 100644 index 0000000000..ae6686c7ea Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/NestedApp.dmg.zip differ diff --git a/Library/Homebrew/cask/test/support/binaries/bzipped_asset.bz2 b/Library/Homebrew/cask/test/support/binaries/bzipped_asset.bz2 new file mode 100644 index 0000000000..6ec60f31f7 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/bzipped_asset.bz2 differ diff --git a/Library/Homebrew/cask/test/support/binaries/cabcontainer.cab b/Library/Homebrew/cask/test/support/binaries/cabcontainer.cab new file mode 100644 index 0000000000..f1dec52dcc Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/cabcontainer.cab differ diff --git a/Library/Homebrew/cask/test/support/binaries/caffeine.zip b/Library/Homebrew/cask/test/support/binaries/caffeine.zip new file mode 100644 index 0000000000..74aca21db6 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/caffeine.zip differ diff --git a/Library/Homebrew/cask/test/support/binaries/caffeine_suite.zip b/Library/Homebrew/cask/test/support/binaries/caffeine_suite.zip new file mode 100644 index 0000000000..7fa5c958e5 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/caffeine_suite.zip differ diff --git a/Library/Homebrew/cask/test/support/binaries/empty_directory/.gitignore b/Library/Homebrew/cask/test/support/binaries/empty_directory/.gitignore new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Library/Homebrew/cask/test/support/binaries/gzipped_asset.gz b/Library/Homebrew/cask/test/support/binaries/gzipped_asset.gz new file mode 100644 index 0000000000..1ff300b4b6 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/gzipped_asset.gz differ diff --git a/Library/Homebrew/cask/test/support/binaries/lzma-asset.lzma b/Library/Homebrew/cask/test/support/binaries/lzma-asset.lzma new file mode 100644 index 0000000000..2ff7ccee7f Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/lzma-asset.lzma differ diff --git a/Library/Homebrew/cask/test/support/binaries/naked_executable b/Library/Homebrew/cask/test/support/binaries/naked_executable new file mode 100755 index 0000000000..039e4d0069 --- /dev/null +++ b/Library/Homebrew/cask/test/support/binaries/naked_executable @@ -0,0 +1,2 @@ +#!/bin/sh +exit 0 diff --git a/Library/Homebrew/cask/test/support/binaries/rarcontainer.rar b/Library/Homebrew/cask/test/support/binaries/rarcontainer.rar new file mode 100644 index 0000000000..9886993069 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/rarcontainer.rar differ diff --git a/Library/Homebrew/cask/test/support/binaries/sevenzipcontainer.7z b/Library/Homebrew/cask/test/support/binaries/sevenzipcontainer.7z new file mode 100644 index 0000000000..226f620cd1 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/sevenzipcontainer.7z differ diff --git a/Library/Homebrew/cask/test/support/binaries/sheldonmac.sit b/Library/Homebrew/cask/test/support/binaries/sheldonmac.sit new file mode 100644 index 0000000000..8814fc31ee Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/sheldonmac.sit differ diff --git a/Library/Homebrew/cask/test/support/binaries/tarball.tgz b/Library/Homebrew/cask/test/support/binaries/tarball.tgz new file mode 100644 index 0000000000..0a9fb56957 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/tarball.tgz differ diff --git a/Library/Homebrew/cask/test/support/binaries/transmission-2.61.dmg b/Library/Homebrew/cask/test/support/binaries/transmission-2.61.dmg new file mode 100644 index 0000000000..b9629f837c Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/transmission-2.61.dmg differ diff --git a/Library/Homebrew/cask/test/support/binaries/xarcontainer.xar b/Library/Homebrew/cask/test/support/binaries/xarcontainer.xar new file mode 100644 index 0000000000..bd37cd4d66 Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/xarcontainer.xar differ diff --git a/Library/Homebrew/cask/test/support/binaries/xzipped-asset.xz b/Library/Homebrew/cask/test/support/binaries/xzipped-asset.xz new file mode 100644 index 0000000000..74db22090b Binary files /dev/null and b/Library/Homebrew/cask/test/support/binaries/xzipped-asset.xz differ diff --git a/Library/Homebrew/cask/test/support/cleanup.rb b/Library/Homebrew/cask/test/support/cleanup.rb new file mode 100644 index 0000000000..0cb9c02fac --- /dev/null +++ b/Library/Homebrew/cask/test/support/cleanup.rb @@ -0,0 +1,12 @@ +module Hbc::CleanupHooks + def after_teardown + super + Hbc.installed.each do |cask| + Hbc::Installer.new(cask).purge_versioned_files + end + end +end + +class MiniTest::Spec + include Hbc::CleanupHooks +end diff --git a/Library/Homebrew/cask/test/support/fake_dirs.rb b/Library/Homebrew/cask/test/support/fake_dirs.rb new file mode 100644 index 0000000000..89612f850a --- /dev/null +++ b/Library/Homebrew/cask/test/support/fake_dirs.rb @@ -0,0 +1,29 @@ +# wire in a set of fake link dirs per-test +module FakeDirHooks + DIRS = [:appdir, :qlplugindir, :binarydir].freeze + + def before_setup + super + + @canned_dirs = {} + + DIRS.each do |dir_name| + dir = Hbc.homebrew_prefix.join("#{dir_name}-#{Time.now.to_i}-#{rand(1024)}") + dir.mkpath + Hbc.send("#{dir_name}=", dir) + @canned_dirs[:dir_name] = dir + end + end + + def after_teardown + super + + @canned_dirs.each_value do |dir| + dir.rmtree if dir.exist? + end + end +end + +class MiniTest::Spec + include FakeDirHooks +end diff --git a/Library/Homebrew/cask/test/support/fake_fetcher.rb b/Library/Homebrew/cask/test/support/fake_fetcher.rb new file mode 100644 index 0000000000..a49a89f7f4 --- /dev/null +++ b/Library/Homebrew/cask/test/support/fake_fetcher.rb @@ -0,0 +1,35 @@ +class Hbc::FakeFetcher + def self.fake_response_for(url, response) + @responses[url] = response + end + + def self.head(url) + @responses ||= {} + raise("no response faked for #{url.inspect}") unless @responses.key?(url) + @responses[url] + end + + def self.init + @responses = {} + end + + def self.clear + @responses = {} + end +end + +module FakeFetcherHooks + def before_setup + super + Hbc::FakeFetcher.init + end + + def after_teardown + super + Hbc::FakeFetcher.clear + end +end + +class MiniTest::Spec + include FakeFetcherHooks +end diff --git a/Library/Homebrew/cask/test/support/fake_system_command.rb b/Library/Homebrew/cask/test/support/fake_system_command.rb new file mode 100644 index 0000000000..e19330fd04 --- /dev/null +++ b/Library/Homebrew/cask/test/support/fake_system_command.rb @@ -0,0 +1,73 @@ +class Hbc::FakeSystemCommand + def self.responses + @responses ||= {} + end + + def self.expectations + @expectations ||= {} + end + + def self.system_calls + @system_calls ||= Hash.new(0) + end + + def self.clear + @responses = nil + @expectations = nil + @system_calls = nil + end + + def self.stubs_command(command, response = "") + responses[command] = response + end + + def self.expects_command(command, response = "", times = 1) + stubs_command(command, response) + expectations[command] = times + end + + def self.expect_and_pass_through(command, times = 1) + pass_through = ->(cmd, opts) { Hbc::SystemCommand.run(cmd, opts) } + expects_command(command, pass_through, times) + end + + def self.verify_expectations! + expectations.each do |command, times| + unless system_calls[command] == times + raise("expected #{command.inspect} to be run #{times} times, but got #{system_calls[command]}") + end + end + end + + def self.run(command_string, options = {}) + command = Hbc::SystemCommand.new(command_string, options).command + unless responses.key?(command) + raise("no response faked for #{command.inspect}, faked responses are: #{responses.inspect}") + end + system_calls[command] += 1 + + response = responses[command] + if response.respond_to?(:call) + response.call(command_string, options) + else + Hbc::SystemCommand::Result.new(command, response, "", 0) + end + end + + def self.run!(command, options = {}) + run(command, options.merge(must_succeed: true)) + end +end + +module FakeSystemCommandHooks + def after_teardown + super + Hbc::FakeSystemCommand.verify_expectations! + ensure + Hbc::FakeSystemCommand.clear + end +end + +class MiniTest::Spec + include FakeSystemCommandHooks +end diff --git a/Library/Homebrew/cask/test/support/never_sudo_system_command.rb b/Library/Homebrew/cask/test/support/never_sudo_system_command.rb new file mode 100644 index 0000000000..50f510a7a5 --- /dev/null +++ b/Library/Homebrew/cask/test/support/never_sudo_system_command.rb @@ -0,0 +1,5 @@ +class Hbc::NeverSudoSystemCommand < Hbc::SystemCommand + def self.run(command, options = {}) + super(command, options.merge(sudo: false)) + end +end diff --git a/Library/Homebrew/cask/test/support/shared_examples.rb b/Library/Homebrew/cask/test/support/shared_examples.rb new file mode 100644 index 0000000000..e846af4047 --- /dev/null +++ b/Library/Homebrew/cask/test/support/shared_examples.rb @@ -0,0 +1,21 @@ +# Adapted from https://gist.github.com/jodosha/1560208 +MiniTest::Spec.class_eval do + def self.shared_examples + @shared_examples ||= {} + end +end + +module MiniTest::Spec::SharedExamples + def shared_examples_for(desc, &block) + MiniTest::Spec.shared_examples[desc] = block + end + + def it_behaves_like(desc, *args, &block) + instance_exec(*args, &MiniTest::Spec.shared_examples[desc]) + instance_eval(&block) if block_given? + end +end + +Object.class_eval do + include(MiniTest::Spec::SharedExamples) +end diff --git a/Library/Homebrew/cask/test/support/shared_examples/dsl_base.rb b/Library/Homebrew/cask/test/support/shared_examples/dsl_base.rb new file mode 100644 index 0000000000..28115bb4b5 --- /dev/null +++ b/Library/Homebrew/cask/test/support/shared_examples/dsl_base.rb @@ -0,0 +1,23 @@ +require "test_helper" + +shared_examples_for Hbc::DSL::Base do + it "supports the token method" do + dsl.token.must_equal cask.token + end + + it "supports the version method" do + dsl.version.must_equal cask.version + end + + it "supports the caskroom_path method" do + dsl.caskroom_path.must_equal cask.caskroom_path + end + + it "supports the staged_path method" do + dsl.staged_path.must_equal cask.staged_path + end + + it "supports the appdir method" do + dsl.appdir.must_equal cask.appdir + end +end diff --git a/Library/Homebrew/cask/test/support/shared_examples/staged.rb b/Library/Homebrew/cask/test/support/shared_examples/staged.rb new file mode 100644 index 0000000000..ef4ff03418 --- /dev/null +++ b/Library/Homebrew/cask/test/support/shared_examples/staged.rb @@ -0,0 +1,109 @@ +require "test_helper" + +shared_examples_for Hbc::Staged do + let(:fake_pathname_exists) { + fake_pathname = Pathname("/path/to/file/that/exists") + fake_pathname.stubs(exist?: true, expand_path: fake_pathname) + fake_pathname + } + + let(:fake_pathname_does_not_exist) { + fake_pathname = Pathname("/path/to/file/that/does/not/exist") + fake_pathname.stubs(exist?: false, expand_path: fake_pathname) + fake_pathname + } + + it "can run system commands with list-form arguments" do + Hbc::FakeSystemCommand.expects_command( + ["echo", "homebrew-cask", "rocks!"] + ) + staged.system_command("echo", args: ["homebrew-cask", "rocks!"]) + end + + it "can get the Info.plist file for the primary app" do + staged.info_plist_file.to_s.must_include Hbc.appdir.join("TestCask.app/Contents/Info.plist") + end + + it "can execute commands on the Info.plist file" do + staged.stubs(bundle_identifier: "com.example.BasicCask") + + Hbc::FakeSystemCommand.expects_command( + ["/usr/libexec/PlistBuddy", "-c", "Print CFBundleIdentifier", staged.info_plist_file] + ) + staged.plist_exec("Print CFBundleIdentifier") + end + + it "can set a key in the Info.plist file" do + staged.stubs(bundle_identifier: "com.example.BasicCask") + + Hbc::FakeSystemCommand.expects_command( + ["/usr/libexec/PlistBuddy", "-c", "Set :JVMOptions:JVMVersion 1.6+", staged.info_plist_file] + ) + staged.plist_set(":JVMOptions:JVMVersion", "1.6+") + end + + it "can set the permissions of a file" do + fake_pathname = fake_pathname_exists + staged.stubs(Pathname: fake_pathname) + + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/sudo", "-E", "--", "/bin/chmod", "-R", "--", "777", fake_pathname] + ) + staged.set_permissions(fake_pathname.to_s, "777") + end + + it "can set the permissions of multiple files" do + fake_pathname = fake_pathname_exists + staged.stubs(:Pathname).returns(fake_pathname) + + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/sudo", "-E", "--", "/bin/chmod", "-R", "--", "777", fake_pathname, fake_pathname] + ) + staged.set_permissions([fake_pathname.to_s, fake_pathname.to_s], "777") + end + + it "cannot set the permissions of a file that does not exist" do + fake_pathname = fake_pathname_does_not_exist + staged.stubs(Pathname: fake_pathname) + staged.set_permissions(fake_pathname.to_s, "777") + end + + it "can set the ownership of a file" do + staged.stubs(current_user: "fake_user") + fake_pathname = fake_pathname_exists + staged.stubs(Pathname: fake_pathname) + + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/sudo", "-E", "--", "/usr/sbin/chown", "-R", "--", "fake_user:staff", fake_pathname] + ) + staged.set_ownership(fake_pathname.to_s) + end + + it "can set the ownership of multiple files" do + staged.stubs(current_user: "fake_user") + fake_pathname = fake_pathname_exists + staged.stubs(Pathname: fake_pathname) + + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/sudo", "-E", "--", "/usr/sbin/chown", "-R", "--", "fake_user:staff", fake_pathname, fake_pathname] + ) + staged.set_ownership([fake_pathname.to_s, fake_pathname.to_s]) + end + + it "can set the ownership of a file with a different user and group" do + fake_pathname = fake_pathname_exists + staged.stubs(Pathname: fake_pathname) + + Hbc::FakeSystemCommand.expects_command( + ["/usr/bin/sudo", "-E", "--", "/usr/sbin/chown", "-R", "--", "other_user:other_group", fake_pathname] + ) + staged.set_ownership(fake_pathname.to_s, user: "other_user", group: "other_group") + end + + it "cannot set the ownership of a file that does not exist" do + staged.stubs(current_user: "fake_user") + fake_pathname = fake_pathname_does_not_exist + staged.stubs(Pathname: fake_pathname) + staged.set_ownership(fake_pathname.to_s) + end +end diff --git a/Library/Homebrew/cask/test/syntax_test.rb b/Library/Homebrew/cask/test/syntax_test.rb new file mode 100644 index 0000000000..0ea832bde6 --- /dev/null +++ b/Library/Homebrew/cask/test/syntax_test.rb @@ -0,0 +1,17 @@ +require "test_helper" + +describe "Syntax check" do + project_root = Pathname.new(File.expand_path("#{File.dirname(__FILE__)}/../")) + backend_files = Dir[project_root.join("**", "*.rb")].reject { |f| f.match %r{/vendor/|/Casks/} } + interpreter = RUBY_PATH + flags = %w[-c] + flags.unshift "--disable-all" + backend_files.each do |file| + it "#{file} is valid Ruby" do + args = flags + ["--", file] + shutup do + raise SyntaxError, "#{file} failed syntax check" unless system(interpreter, *args) + end + end + end +end diff --git a/Library/Homebrew/cask/test/test_helper.rb b/Library/Homebrew/cask/test/test_helper.rb new file mode 100644 index 0000000000..528ca69cf8 --- /dev/null +++ b/Library/Homebrew/cask/test/test_helper.rb @@ -0,0 +1,184 @@ +require "bundler" +require "bundler/setup" +require "pathname" + +if ENV["COVERAGE"] + require "coveralls" + Coveralls.wear_merged! +end + +project_root = Pathname.new(File.expand_path("../..", __FILE__)) +tap_root = Pathname.new(ENV["HOMEBREW_LIBRARY"]).join("Taps", "caskroom", "homebrew-cask") + +# add Homebrew to load path +$LOAD_PATH.unshift(File.expand_path("#{ENV['HOMEBREW_REPOSITORY']}/Library/Homebrew")) + +require "global" +require "extend/pathname" + +# add Homebrew-Cask to load path +$LOAD_PATH.push(project_root.join("lib").to_s) + +# force some environment variables +ENV["HOMEBREW_NO_EMOJI"] = "1" +ENV["HOMEBREW_CASK_OPTS"] = nil + +# TODO: temporary, copied from old Homebrew, this method is now moved inside a class +def shutup + if ENV.key?("VERBOSE_TESTS") + yield + else + begin + tmperr = $stderr.clone + tmpout = $stdout.clone + $stderr.reopen "/dev/null", "w" + $stdout.reopen "/dev/null", "w" + yield + ensure + $stderr.reopen tmperr + $stdout.reopen tmpout + end + end +end + +def sudo(*args) + %w[/usr/bin/sudo -E --] + Array(args).flatten +end + +TEST_TMPDIR = Dir.mktmpdir("homebrew_cask_tests") +at_exit do + FileUtils.remove_entry(TEST_TMPDIR) +end + +# must be called after testing_env so at_exit hooks are in proper order +require "minitest/autorun" +require "minitest/reporters" +Minitest::Reporters.use! Minitest::Reporters::DefaultReporter.new(color: true) + +# Force mocha to patch MiniTest since we have both loaded thanks to homebrew's testing_env +require "mocha/api" +require "mocha/integration/mini_test" +Mocha::Integration::MiniTest.activate + +# our baby +require "hbc" + +# override Homebrew locations +Hbc.homebrew_prefix = Pathname.new(TEST_TMPDIR).join("prefix") +Hbc.homebrew_repository = Hbc.homebrew_prefix + +# Override Tap::TAP_DIRECTORY to use our test Tap directory. +class Tap + send(:remove_const, :TAP_DIRECTORY) + TAP_DIRECTORY = Hbc.homebrew_prefix.join("Library", "Taps") +end + +Hbc.default_tap = Tap.fetch("caskroom", "testcasks") + +# also jack in some test Casks +FileUtils.ln_s project_root.join("test", "support"), Tap::TAP_DIRECTORY.join("caskroom").tap(&:mkpath).join("homebrew-testcasks") + +# pretend that the caskroom/cask Tap is installed +FileUtils.ln_s tap_root, Tap::TAP_DIRECTORY.join("caskroom").tap(&:mkpath).join("homebrew-cask") + +# create cache directory +Hbc.homebrew_cache = Pathname.new(TEST_TMPDIR).join("cache") +Hbc.cache.mkpath + +# our own testy caskroom +Hbc.caskroom = Hbc.homebrew_prefix.join("TestCaskroom") + +class TestHelper + # helpers for test Casks to reference local files easily + def self.local_binary_path(name) + File.expand_path(File.join(File.dirname(__FILE__), "support", "binaries", name)) + end + + def self.local_binary_url(name) + "file://" + local_binary_path(name) + end + + def self.test_cask + @test_cask ||= Hbc.load("basic-cask") + end + + def self.fake_fetcher + Hbc::FakeFetcher + end + + def self.fake_response_for(*args) + Hbc::FakeFetcher.fake_response_for(*args) + end + + def self.must_output(test, lambda, expected = nil) + out, err = test.capture_subprocess_io do + lambda.call + end + + if block_given? + yield (out + err).chomp + elsif expected.is_a?(Regexp) + (out + err).chomp.must_match expected + else + (out + err).chomp.must_equal expected.gsub(%r{^ *}, "") + end + end + + def self.valid_alias?(candidate) + return false unless candidate.symlink? + candidate.readlink.exist? + end + + def self.install_without_artifacts(cask) + Hbc::Installer.new(cask).tap do |i| + shutup do + i.download + i.extract_primary_container + end + end + end + + def self.install_with_caskfile(cask) + Hbc::Installer.new(cask).tap do |i| + shutup do + i.save_caskfile + end + end + end + + def self.install_without_artifacts_with_caskfile(cask) + Hbc::Installer.new(cask).tap do |i| + shutup do + i.download + i.extract_primary_container + i.save_caskfile + end + end + end +end + +# Extend MiniTest API with support for RSpec-style shared examples +require "support/shared_examples" +require "support/shared_examples/dsl_base.rb" +require "support/shared_examples/staged.rb" + +require "support/fake_fetcher" +require "support/fake_dirs" +require "support/fake_system_command" +require "support/cleanup" +require "support/never_sudo_system_command" +require "tmpdir" +require "tempfile" + +# create directories +FileUtils.mkdir_p Hbc.homebrew_prefix.join("bin") + +# Common superclass for test Casks for when we need to filter them out +class Hbc::TestCask < Hbc::Cask; end + +# jack in some optional utilities +FileUtils.ln_s "/usr/local/bin/cabextract", Hbc.homebrew_prefix.join("bin/cabextract") +FileUtils.ln_s "/usr/local/bin/unar", Hbc.homebrew_prefix.join("bin/unar") +FileUtils.ln_s "/usr/local/bin/unlzma", Hbc.homebrew_prefix.join("bin/unlzma") +FileUtils.ln_s "/usr/local/bin/unxz", Hbc.homebrew_prefix.join("bin/unxz") +FileUtils.ln_s "/usr/local/bin/lsar", Hbc.homebrew_prefix.join("bin/lsar")