`brew cask ci`

This commit is contained in:
Markus Reiter 2018-08-01 18:39:26 +02:00
parent f75cd99870
commit 3cc78d4471
10 changed files with 441 additions and 164 deletions

View File

@ -16,14 +16,39 @@ branches:
cache:
directories:
- /usr/local/Homebrew/Library/Homebrew/vendor/bundle
- /usr/local/Homebrew/Library/Homebrew/vendor/portable-ruby
install: true # skip install step
install:
- |
# Force strict error checking.
set -o errexit
set -o pipefail
- |
# Update Travis commit range.
# This is not normally required but does prevent problems with outdated forks and
# deleted casks (see https://github.com/Homebrew/homebrew-cask/pull/43164).
BRANCH_COMMIT="${TRAVIS_COMMIT_RANGE##*.}"
TARGET_COMMIT="${TRAVIS_COMMIT_RANGE%%.*}"
if ! MERGE_BASE="$(git merge-base "${BRANCH_COMMIT}" "${TARGET_COMMIT}" 2>/dev/null)"; then
git fetch --unshallow
MERGE_BASE="$(git merge-base "${BRANCH_COMMIT}" "${TARGET_COMMIT}")"
fi
export TRAVIS_COMMIT_RANGE="${MERGE_BASE}...${BRANCH_COMMIT}"
- |
# Switch to master branch.
export HOMEBREW_COLOR=1
export HOMEBREW_DEVELOPER=1
export HOMEBREW_NO_AUTO_UPDATE=1
brew update
- |
# Mirror the repo as a tap.
TAP_DIR="$(brew --repository)/Library/Taps/${TRAVIS_REPO_SLUG}"
mkdir -p "${TAP_DIR}"
rsync -az --delete "${TRAVIS_BUILD_DIR}/" "${TAP_DIR}/"
export TRAVIS_BUILD_DIR="${TAP_DIR}"
builtin cd "${TRAVIS_BUILD_DIR}"
before_script:
- . ci/travis/before_script.sh
script:
- . ci/travis/script.sh
script: brew cask ci
notifications:
email: false

View File

@ -1,5 +0,0 @@
## Travis-CI Scripts
The bash scripts in this directory are run only in Travis-CI, and are placed here to help simplify the [`.travis.yml`](../../.travis.yml) configuration file.
These scripts are not meant to be run locally by users or developers of Homebrew-Cask.

View File

@ -1,36 +0,0 @@
#!/usr/bin/env bash
#
# before_script.sh
#
# This file is meant to be sourced during the `before_script` phase of the
# Travis build. Do not attempt to source or run it locally.
#
# shellcheck disable=SC1090
. "${TRAVIS_BUILD_DIR}/ci/travis/helpers.sh"
header 'Running before_script.sh...'
# this is not normally required but does prevent problems with outdated forks and/or deleted casks
# see https://github.com/Homebrew/homebrew-cask/pull/43164
run export BRANCH_COMMIT="${TRAVIS_COMMIT_RANGE##*.}"
run export TARGET_COMMIT="${TRAVIS_COMMIT_RANGE%%.*}"
# shellcheck disable=SC2016
if ! run 'MERGE_BASE="$(git merge-base "${BRANCH_COMMIT}" "${TARGET_COMMIT}")"'; then
run git fetch --unshallow
run 'MERGE_BASE="$(git merge-base "${BRANCH_COMMIT}" "${TARGET_COMMIT}")"'
fi
run export MERGE_BASE="${MERGE_BASE}"
run export TRAVIS_COMMIT_RANGE="${MERGE_BASE}...${BRANCH_COMMIT}"
# make sure brew is on master branch
run export HOMEBREW_DEVELOPER=1
# update homebrew
run brew update
# mirror the repo as a tap, then run the build from there
run export CASK_TAP_DIR="$(brew --repository)/Library/Taps/${TRAVIS_REPO_SLUG}"
run mkdir -p "${CASK_TAP_DIR}"
run rsync -az --delete "${TRAVIS_BUILD_DIR}/" "${CASK_TAP_DIR}/"
run export TRAVIS_BUILD_DIR="${CASK_TAP_DIR}"
run cd "${CASK_TAP_DIR}" || exit 1

View File

@ -1,49 +0,0 @@
#!/usr/bin/env bash
#
# helpers.sh
#
# Helper functions for Travis build scripts.
#
# force strict error checking
set -o errexit
set -o pipefail
# enable extended globbing syntax
shopt -s extglob
CYAN='\033[0;36m'
MAGENTA='\033[1;35m'
RED='\033[1;31m'
YELLOW='\033[0;33m'
NC='\033[0m' # no color
# log command before running and add a blank line
run () {
ohai "$*"
eval "$*"
local retval=$?
echo
return $retval
}
ohai () {
echo -e "${MAGENTA}>>>${NC} $*"
}
onoe () {
echo -e "${YELLOW}>>> $* ${NC}"
exit 0
}
odie () {
echo -e "${RED}!!! $* !!!${NC}"
exit 1
}
# print args as a cyan header
header () {
echo
echo -e "${CYAN}$*${NC}"
echo
}

View File

@ -1,68 +0,0 @@
#!/usr/bin/env bash
#
# script.sh
#
# This file is meant to be sourced during the `script` phase of the Travis
# build. Do not attempt to source or run it locally.
#
# shellcheck disable=SC1090
. "${TRAVIS_BUILD_DIR}/ci/travis/helpers.sh"
header 'Running script.sh...'
apps () { /usr/bin/find /Applications -type d -name '*.app' -maxdepth 2 ; }
launchjob_install () { "$(brew --repository)/Library/Taps/Homebrew/homebrew-cask/developer/bin/list_installed_launchjob_ids" ; }
launchjob_load () { "$(brew --repository)/Library/Taps/Homebrew/homebrew-cask/developer/bin/list_loaded_launchjob_ids" ; }
pkgs () { "$(brew --repository)/Library/Taps/Homebrew/homebrew-cask/developer/bin/list_recent_pkg_ids" ; }
checks=('pkgs' 'apps' 'launchjob_install' 'launchjob_load')
/bin/mkdir -p "${HOME}/cask-checks/"{before,after}
for check in "${checks[@]}"; do
"${check}" > "${HOME}/cask-checks/before/${check}"
done
modified_casks=($(git diff --name-only --diff-filter=AMR "${TRAVIS_COMMIT_RANGE}" -- Casks/*.rb))
run export HOMEBREW_NO_AUTO_UPDATE=1
if [[ ${#modified_casks[@]} -eq 0 ]]; then
onoe 'No Casks modified, skipping'
fi
run brew cask style "${modified_casks[@]}"
if [[ "${TRAVIS_REPO_SLUG}" != 'Homebrew/homebrew-cask-fonts' ]]; then
if [[ ${#modified_casks[@]} -gt 1 ]]; then
run brew cask audit "${modified_casks[@]}"
odie "More than 1 Cask modified, didn't check Cask checksums or URLs"
fi
fi
run brew cask _audit_modified_casks "${TRAVIS_COMMIT_RANGE}"
if /usr/bin/grep --quiet "depends_on cask:" "${modified_casks[@]}"; then
run brew tap homebrew/bundle
run brew bundle dump --file="${HOME}/Brewfile"
fi
for cask in "${modified_casks[@]}"; do
run brew cask reinstall --verbose "${cask}"
run brew cask uninstall --verbose "${cask}"
done
if [[ -f "${HOME}/Brewfile" ]]; then
run brew bundle cleanup --force --file="${HOME}/Brewfile"
fi
sleep 5 # Rerunning the checks too soon can result in false positives
for check in "${checks[@]}"; do
"${check}" > "${HOME}/cask-checks/after/${check}"
if ! /usr/bin/diff "${HOME}/cask-checks/before/${check}" "${HOME}/cask-checks/after/${check}" > /dev/null; then
ohai "Leftover: ${check}"
/usr/bin/diff "${HOME}/cask-checks/before/${check}" "${HOME}/cask-checks/after/${check}" | /usr/bin/grep '>'
fi
done

252
cmd/brewcask-ci.rb Executable file
View File

@ -0,0 +1,252 @@
# frozen_string_literal: false
require "utils/github"
require "utils/formatter"
require_relative "lib/capture"
require_relative "lib/diffable"
require_relative "lib/github"
require_relative "lib/travis"
module Hbc
class CLI
class Ci < AbstractCommand
def run
unless ENV.key?("TRAVIS")
raise CaskError, "This command isnt meant to be run locally."
end
unless tap
raise CaskError, "This command must be run from inside a tap directory."
end
ruby_files_in_wrong_directory = modified_ruby_files - (modified_cask_files + modified_command_files)
unless ruby_files_in_wrong_directory.empty?
raise CaskError, "Casks are in the wrong directory:\n" +
ruby_files_in_wrong_directory.join("\n")
end
if modified_cask_files.count > 1 && pr_author && !maintainers.include?(pr_author)
raise CaskError, "More than one cask modified; please submit a pull request for each cask separately."
end
overall_success = true
modified_cask_files.each do |path|
cask = CaskLoader.load(path)
overall_success &= step "brew cask audit #{cask.token}", "audit" do
Auditor.audit(cask, audit_download: true,
check_token_conflicts: added_cask_files.include?(path),
commit_range: ENV["TRAVIS_COMMIT_RANGE"])
end
overall_success &= step "brew cask style #{cask.token}", "style" do
Style.run(path)
end
was_installed = cask.installed?
cask_dependencies = CaskDependencies.new(cask).reject(&:installed?)
checks = {
installed_apps: Diffable.new do
sleep(5) # Allow `mdfind` to refresh.
system_command!("/usr/bin/mdfind", args: ["-onlyin", "/", "kMDItemContentType == com.apple.application-bundle"], print_stderr: false)
.stdout
.split("\n")
end,
installed_kexts: Diffable.new do
system_command!("/usr/sbin/kextstat", args: ["-kl"], print_stderr: false)
.stdout
.lines
.map { |l| l.match(/^.{52}([^\s]+)/)[1] }
.grep_v(/^com\.apple\./)
end,
installed_launchjobs: Diffable.new do
format_launchjob = lambda { |file|
name = file.basename(".plist").to_s
label = Plist.parse_xml(File.read(file))["Label"]
(name == label) ? name : "#{name} (#{label})"
}
[
"~/Library/LaunchAgents",
"~/Library/LaunchDaemons",
"/Library/LaunchAgents",
"/Library/LaunchDaemons",
].map { |p| Pathname(p).expand_path }
.select(&:directory?)
.flat_map(&:children)
.select { |child| child.extname == ".plist" }
.map(&format_launchjob)
end,
loaded_launchjobs: Diffable.new do
launchctl = lambda do |sudo|
system_command!("/bin/launchctl", args: ["list"], print_stderr: false, sudo: sudo)
.stdout
.lines.drop(1)
end
[false, true]
.flat_map(&launchctl)
.map { |l| l.split(/\s+/)[2] }
.grep_v(/^com\.apple\./)
end,
installed_pkgs: Diffable.new do
Pathname("/var/db/receipts")
.children
.grep(/\.plist$/)
.map(&:basename)
end,
}
overall_success &= step "brew cask install #{cask.token}", "install" do
Installer.new(cask, force: true).uninstall if was_installed
checks.values.each(&:before)
Installer.new(cask, verbose: true).install
end
overall_success &= step "brew cask uninstall #{cask.token}", "uninstall" do
success = begin
Installer.new(cask, verbose: true).uninstall
true
rescue => e
$stderr.puts e.message
$stderr.puts e.backtrace
false
ensure
cask_dependencies.each do |c|
Installer.new(c, verbose: true).uninstall if c.installed?
end
end
checks.each do |name, check|
check.after
next unless check.changed?
success = false
message = case name
when :installed_pkgs
"Some packages were not uninstalled."
when :loaded_launchjobs
"Some launch jobs were not unloaded."
when :installed_launchjobs
"Some launch jobs were not uninstalled."
when :installed_kexts
"Some kernel extensions were not uninstalled."
when :installed_apps
"Some applications were not uninstalled."
end
$stderr.puts Formatter.error(message, label: "Error")
$stderr.puts check.diff_lines.join("\n")
end
success
end
end
if overall_success
puts Formatter.success("Build finished successfully.", label: "Success")
return
end
raise CaskError, "Build failed."
end
private
def step(name, travis_id)
success = false
output = nil
Travis.fold travis_id do
print "#{Tty.bold}#{Tty.yellow}#{name}#{Tty.reset} "
success, output = capture do
begin
yield != false
rescue => e
$stderr.puts e.message
false
end
end
if success
puts Formatter.success("")
puts output
else
puts Formatter.error("")
end
end
puts output unless success
success
ensure
$stdout.flush
$stderr.flush
end
def tap
@tap ||= if ENV.key?("TRAVIS_REPO_SLUG")
Tap.fetch(ENV["TRAVIS_REPO_SLUG"])
else
Tap.from_path(Dir.pwd)
end
end
def pr_author
return unless ENV.key?("TRAVIS_PULL_REQUEST")
return unless ENV.key?("TRAVIS_REPO_SLUG")
@pr_author ||= begin
owner, repo = ENV["TRAVIS_REPO_SLUG"].split("/", 2)
pr = GitHub.pull_request(owner, repo, ENV["TRAVIS_PULL_REQUEST"])
pr.dig("user", "login")
end
end
def maintainers
@maintainers ||= begin
GitHub.members("Homebrew", team: "cask").map { |member| member.fetch("login") }
rescue GitHub::AuthenticationFailedError
[]
end
end
def modified_files
@modified_files ||= system_command!(
"git", args: ["diff", "--name-only", "--diff-filter=AMR", ENV["TRAVIS_COMMIT_RANGE"]]
).stdout.split("\n").map { |path| Pathname(path) }
end
def added_files
@added_files ||= system_command!(
"git", args: ["diff", "--name-only", "--diff-filter=A", ENV["TRAVIS_COMMIT_RANGE"]]
).stdout.split("\n").map { |path| Pathname(path) }
end
def modified_ruby_files
@modified_ruby_files ||= modified_files.select { |path| path.extname == ".rb" }
end
def modified_command_files
@modified_command_files ||= modified_files.select { |path| tap.command_file?(path) || path.ascend.to_a.last.to_s == "cmd" }
end
def modified_cask_files
@modified_cask_files ||= modified_files.select { |path| tap.cask_file?(path) }
end
def added_cask_files
@added_cask_files ||= added_files.select { |path| tap.cask_file?(path) }
end
end
end
end

38
cmd/lib/capture.rb Normal file
View File

@ -0,0 +1,38 @@
require "pty"
def capture
old_stdout = $stdout.dup
old_stderr = $stderr.dup
PTY.open do |r, w|
$stdout.reopen(w)
$stderr.reopen(w)
thread = Thread.new do
begin
yield
ensure
$stdout.flush
$stderr.flush
end
end
thread.abort_on_exception = true
output = ""
loop do
begin
output << r.readline_nonblock || ""
rescue IO::WaitReadable
break unless thread.alive?
end
end
result = thread.value
[result, output]
end
ensure
$stdout.reopen(old_stdout)
$stderr.reopen(old_stderr)
end

51
cmd/lib/diffable.rb Normal file
View File

@ -0,0 +1,51 @@
class Diffable
def initialize(&block)
@before = nil
@after = nil
@gather = block
end
def gather
@gather.call.sort.uniq
end
def before
@before ||= gather
end
def after
@after ||= gather
end
def diff
removed = before.reject { |e| after.include?(e) }
added = after - before
[removed, added]
end
def changed?
removed, added = diff
removed.any? || added.any?
end
def combined
(before + after).sort.uniq
end
def diff_lines(skip_unchanged: true)
removed, added = diff
lines = combined.flat_map do |e|
if removed.include?(e)
"#{Tty.red}- #{e}#{Tty.reset}"
elsif added.include?(e)
"#{Tty.green}+ #{e}#{Tty.reset}"
else
skip_unchanged ? [] : " #{e}"
end
end
lines
end
end

25
cmd/lib/github.rb Normal file
View File

@ -0,0 +1,25 @@
module GitHub
module_function
ORG_READ_ACCESS_SCOPES = ["read:org"].freeze
def members(org, team: nil)
if team
url = "#{API_URL}/orgs/#{org}/teams"
teams = open_api(url, scopes: ORG_READ_ACCESS_SCOPES)
team = teams.detect { |t| t["name"] == team }
return [] unless team
open_api("#{team["url"]}/members", scopes: ORG_READ_ACCESS_SCOPES)
else
url = "#{API_URL}/orgs/#{org}/members"
open_api(url, scopes: CREATE_ISSUE_FORK_OR_PR_SCOPES)
end
end
def pull_request(owner, repo, number)
url = "#{API_URL}/repos/#{owner}/#{repo}/pulls/#{number}"
open_api(url)
end
end

44
cmd/lib/travis.rb Normal file
View File

@ -0,0 +1,44 @@
module Travis
module_function
@start = {}
def fold(id, &block)
begin
puts fold_start(id)
time(rand(2**32).to_s(16), &block)
ensure
puts fold_end(id)
$stdout.flush
$stderr.flush
end
end
def fold_start(id)
"travis_fold:start:#{id}"
end
def fold_end(id)
"travis_fold:end:#{id}"
end
def time(id)
puts time_start(id)
yield
ensure
puts time_end(id)
end
def time_start(id)
@start[id] = Time.now
"travis_time:start:#{id}"
end
def time_end(id)
start = (@start[id].to_f * 1_000_000_000).to_i
finish = (Time.now.to_f * 1_000_000_000).to_i
duration = finish - start
"travis_time:end:#{id},start=#{start},finish=#{finish},duration=#{duration}"
end
end