homebrew_automation 0.0.4 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bin/homebrew_automation.rb +73 -7
- data/lib/homebrew_automation/bintray.rb +86 -0
- data/lib/homebrew_automation/bottle.rb +74 -0
- data/lib/homebrew_automation/bottle_gatherer.rb +51 -0
- data/lib/homebrew_automation/formula.rb +217 -0
- data/lib/homebrew_automation/mac-os.rb +29 -0
- data/lib/homebrew_automation/source_dist.rb +52 -0
- data/lib/homebrew_automation/tap.rb +85 -0
- data/lib/homebrew_automation/version.rb +1 -1
- data/lib/homebrew_automation/workflow.rb +91 -0
- data/lib/homebrew_automation.rb +11 -169
- metadata +55 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 97e03de71e29414ed00f37914b8964e91421465de75ffd1cd39c85a4ceb274da
|
4
|
+
data.tar.gz: da6cf8c8bd4e2ad86ccd2f0242c0407ef2955b74e5fc70c8b0fbf39e1fa6ffe9
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6ee552dac8c240c7585073046f2c6c1385bbd65802146aa55bd1e6bd074b9d122d8e018b2135ca13ee053df4f3bd113b52f687906bd08858748bdf0f03f1d83d
|
7
|
+
data.tar.gz: 6dd74ac44f8d465976426baf3f201ca5786d15e9f9c557610e19297d870764eef511390cfcbeeb1e7d5a5b5eb7ccba2b0d242668129e21c3bf765ec513030e38
|
data/bin/homebrew_automation.rb
CHANGED
@@ -2,22 +2,20 @@
|
|
2
2
|
|
3
3
|
require 'thor'
|
4
4
|
|
5
|
-
|
5
|
+
require_relative '../lib/homebrew_automation.rb'
|
6
6
|
|
7
|
-
class
|
7
|
+
class FormulaCommands < Thor
|
8
8
|
|
9
|
-
desc 'put-sdist', '
|
9
|
+
desc 'put-sdist', 'Update the URL and sha256 checksum of the source tarball'
|
10
10
|
option :url, :required => true
|
11
11
|
option :sha256, :required => true
|
12
12
|
def put_sdist
|
13
13
|
before = HomebrewAutomation::Formula.parse_string($stdin.read)
|
14
|
-
after = before.
|
15
|
-
update_field("url", options[:url]).
|
16
|
-
update_field("sha256", options[:sha256])
|
14
|
+
after = before.put_sdist options[:url], options[:sha256]
|
17
15
|
$stdout.write after
|
18
16
|
end
|
19
17
|
|
20
|
-
desc 'put-bottle', '
|
18
|
+
desc 'put-bottle', 'Insert or update a bottle reference for a given OS'
|
21
19
|
option :os, :required => true
|
22
20
|
option :sha256, :required => true
|
23
21
|
def put_bottle
|
@@ -28,6 +26,74 @@ class MyCliApp < Thor
|
|
28
26
|
|
29
27
|
end
|
30
28
|
|
29
|
+
class WorkflowCommands < Thor
|
30
|
+
class_option :tap_user, :required => true
|
31
|
+
class_option :tap_repo, :required => true
|
32
|
+
class_option :tap_token, :required => true
|
33
|
+
class_option :bintray_user, :required => true
|
34
|
+
class_option :bintray_token, :required => true
|
35
|
+
|
36
|
+
desc 'build-and-upload', 'Build binary tarball from source tarball, then upload to Bintray'
|
37
|
+
long_desc <<-HERE_HERE
|
38
|
+
Since we're uploading to Bintray, we need a Bintray API KEY at `bintray_token`.
|
39
|
+
|
40
|
+
`formula_name` defaults to the same as `source_repo`.
|
41
|
+
`formula_version` defaults to `source_tag` with a leading `v` stripped off.
|
42
|
+
HERE_HERE
|
43
|
+
option :source_user, :required => true
|
44
|
+
option :source_repo, :required => true
|
45
|
+
option :source_tag, :required => true
|
46
|
+
option :formula_name
|
47
|
+
option :formula_version
|
48
|
+
def build_and_upload
|
49
|
+
workflow.build_and_upload_bottle(
|
50
|
+
HomebrewAutomation::SourceDist.new(
|
51
|
+
options[:source_user],
|
52
|
+
options[:source_repo],
|
53
|
+
options[:source_tag]),
|
54
|
+
formula_name: options[:formula_name],
|
55
|
+
version_name: options[:formula_version])
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'gather-and-publish', 'Make the Tap aware of new Bottles'
|
59
|
+
long_desc <<-HERE_HERE
|
60
|
+
See what bottles have been built and uploaded to Bintray, then publish them into the Tap.
|
61
|
+
|
62
|
+
Since we're publishing updates to the Formula in our Tap, we need Git push access to the
|
63
|
+
Tap repo on Github via a Github OAuth token via `tap_token`.
|
64
|
+
|
65
|
+
`formula-name` should be both the formula name as appears in the Tap and also the Bintray package name.
|
66
|
+
`formula-version` should be the Bintray "Version" name.
|
67
|
+
HERE_HERE
|
68
|
+
option :formula_name, :required => true
|
69
|
+
option :formula_version, :required => true
|
70
|
+
def gather_and_publish
|
71
|
+
workflow.gather_and_publish_bottles(
|
72
|
+
options[:formula_name],
|
73
|
+
options[:formula_version])
|
74
|
+
end
|
75
|
+
|
76
|
+
private
|
77
|
+
|
78
|
+
def workflow
|
79
|
+
HomebrewAutomation::Workflow.new(
|
80
|
+
HomebrewAutomation::Tap.new(options[:tap_user], options[:tap_repo], options[:tap_token]),
|
81
|
+
HomebrewAutomation::Bintray.new(options[:bintray_user], options[:bintray_token]))
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
class MyCliApp < Thor
|
87
|
+
|
88
|
+
desc 'formula (...)', 'Modify Formula DSL source (read stdin, write stdout)'
|
89
|
+
subcommand "formula", FormulaCommands
|
90
|
+
|
91
|
+
desc 'bottle (...)', 'Workflows for dealing with binary artifacts'
|
92
|
+
subcommand "bottle", WorkflowCommands
|
93
|
+
|
94
|
+
end
|
95
|
+
|
96
|
+
|
31
97
|
MyCliApp.start(ARGV)
|
32
98
|
|
33
99
|
|
@@ -0,0 +1,86 @@
|
|
1
|
+
|
2
|
+
require 'json'
|
3
|
+
require 'base64'
|
4
|
+
require 'uri'
|
5
|
+
require 'rest-client'
|
6
|
+
|
7
|
+
module HomebrewAutomation
|
8
|
+
|
9
|
+
class Bintray
|
10
|
+
|
11
|
+
def initialize(
|
12
|
+
username,
|
13
|
+
api_key,
|
14
|
+
http: RestClient,
|
15
|
+
base_url: "https://bintray.com/api/v1"
|
16
|
+
)
|
17
|
+
@username = username
|
18
|
+
@api_key = api_key
|
19
|
+
@base_url = base_url
|
20
|
+
@http = http # allow injecting mocks for testing
|
21
|
+
end
|
22
|
+
|
23
|
+
# POST /packages/:subject/:repo/:package/versions
|
24
|
+
#
|
25
|
+
# Redundant: Bintray seems to create nonexistant versions for you if you
|
26
|
+
# just try to upload files into it.
|
27
|
+
def create_version(repo_name, package_name, version_name)
|
28
|
+
safe_repo = URI.escape(repo_name)
|
29
|
+
safe_pkg = URI.escape(package_name)
|
30
|
+
@http.post(
|
31
|
+
rel("/packages/#{safe_username}/#{safe_repo}/#{safe_pkg}/versions"),
|
32
|
+
{ name: version_name }.to_json,
|
33
|
+
api_headers
|
34
|
+
)
|
35
|
+
end
|
36
|
+
|
37
|
+
# PUT /content/:subject/:repo/:package/:version/:file_path[?publish=0/1][?override=0/1][?explode=0/1]
|
38
|
+
#
|
39
|
+
# Bintray seems to expect the byte sequence of the file to be written straight out into the
|
40
|
+
# HTTP request body, optionally via `Transfer-Encoding: chunked`. So we pass the `content` String
|
41
|
+
# straight through to RestClient
|
42
|
+
def upload_file(repo_name, package_name, version_name, filename, content, publish: 1)
|
43
|
+
safe_repo = URI.escape(repo_name)
|
44
|
+
safe_pkg = URI.escape(package_name)
|
45
|
+
safe_ver = URI.escape(version_name)
|
46
|
+
safe_filename = URI.escape(filename)
|
47
|
+
safe_publish = URI.escape(publish.to_s)
|
48
|
+
@http.put(
|
49
|
+
rel("/content/#{safe_username}/#{safe_repo}/#{safe_pkg}/#{safe_ver}/#{safe_filename}?publish=#{safe_publish}"),
|
50
|
+
content,
|
51
|
+
auth_headers
|
52
|
+
)
|
53
|
+
end
|
54
|
+
|
55
|
+
# GET /packages/:subject/:repo/:package/versions/:version/files[?include_unpublished=0/1]
|
56
|
+
def get_all_files_in_version(repo_name, package_name, version_name)
|
57
|
+
safe_repo = URI.escape(repo_name)
|
58
|
+
safe_pkg = URI.escape(package_name)
|
59
|
+
safe_ver = URI.escape(version_name)
|
60
|
+
@http.get(
|
61
|
+
rel("/packages/#{safe_username}/#{safe_repo}/#{safe_pkg}/versions/#{safe_ver}/files"),
|
62
|
+
auth_headers)
|
63
|
+
end
|
64
|
+
|
65
|
+
def safe_username
|
66
|
+
URI.escape(@username)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Expand relative path
|
70
|
+
def rel(slash_subpath)
|
71
|
+
@base_url + slash_subpath
|
72
|
+
end
|
73
|
+
|
74
|
+
def api_headers
|
75
|
+
{ "Content-Type" => "application/json" }.update auth_headers
|
76
|
+
end
|
77
|
+
|
78
|
+
def auth_headers
|
79
|
+
# As per RFC 7617
|
80
|
+
cred = Base64.strict_encode64("#{@username}:#{@api_key}")
|
81
|
+
{ Authorization: "Basic #{cred}" }
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
@@ -0,0 +1,74 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
module HomebrewAutomation
|
4
|
+
|
5
|
+
class Bottle
|
6
|
+
|
7
|
+
def initialize(
|
8
|
+
tap_url,
|
9
|
+
formula_name,
|
10
|
+
os_name,
|
11
|
+
filename: nil,
|
12
|
+
content: nil)
|
13
|
+
@tap_url = tap_url
|
14
|
+
@formula_name = formula_name
|
15
|
+
@os_name = os_name
|
16
|
+
@filename = filename
|
17
|
+
@minus_minus = nil # https://github.com/Homebrew/brew/pull/4612
|
18
|
+
@content = content
|
19
|
+
end
|
20
|
+
|
21
|
+
# Takes ages to run, just like if done manually
|
22
|
+
def build
|
23
|
+
die unless system 'brew', 'tap', local_tap_name, @tap_url
|
24
|
+
die unless system 'brew', 'install', '--verbose', '--build-bottle', @formula_name
|
25
|
+
die unless system 'brew', 'bottle', '--verbose', '--json', @formula_name
|
26
|
+
end
|
27
|
+
|
28
|
+
# Read and analyse metadata JSON file
|
29
|
+
def locate_tarball
|
30
|
+
json_filename = Dir['*.bottle.json'].first
|
31
|
+
unless json_filename
|
32
|
+
build
|
33
|
+
return locate_tarball
|
34
|
+
end
|
35
|
+
json = JSON.parse(File.read(json_filename))
|
36
|
+
focus = json || die
|
37
|
+
focus = focus[json.keys.first] || die
|
38
|
+
focus = focus['bottle'] || die
|
39
|
+
focus = focus['tags'] || die
|
40
|
+
focus = focus[@os_name] || die
|
41
|
+
@minus_minus, @filename = focus['local_filename'], focus['filename']
|
42
|
+
end
|
43
|
+
|
44
|
+
def minus_minus
|
45
|
+
@minus_minus || locate_tarball.first
|
46
|
+
end
|
47
|
+
|
48
|
+
def filename
|
49
|
+
@filename || locate_tarball.last
|
50
|
+
end
|
51
|
+
|
52
|
+
def load_tarball_from_disk
|
53
|
+
File.rename minus_minus, filename
|
54
|
+
@content = File.read filename
|
55
|
+
end
|
56
|
+
|
57
|
+
def content
|
58
|
+
@content || load_tarball_from_disk
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
# A name for the temporary tap; doesn't really matter what this is.
|
64
|
+
def local_tap_name
|
65
|
+
'easoncxz/local-tap'
|
66
|
+
end
|
67
|
+
|
68
|
+
def die
|
69
|
+
raise StandardError.new
|
70
|
+
end
|
71
|
+
|
72
|
+
end
|
73
|
+
|
74
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
|
2
|
+
require_relative './mac-os.rb'
|
3
|
+
require_relative './formula.rb'
|
4
|
+
|
5
|
+
module HomebrewAutomation
|
6
|
+
|
7
|
+
# Some functions for figuring out, from files on Bintray, what values to use in bottle DSL.
|
8
|
+
class BottleGatherer
|
9
|
+
|
10
|
+
# @param json [Hash] List of files from Bintray
|
11
|
+
def initialize(json)
|
12
|
+
@json = json
|
13
|
+
@bottles = nil
|
14
|
+
end
|
15
|
+
|
16
|
+
# () -> Hash String String
|
17
|
+
#
|
18
|
+
# Returns a hash with keys being OS names (in Homebrew-form) and values being SHA256 checksums
|
19
|
+
def bottles
|
20
|
+
return @bottles if @bottles
|
21
|
+
pairs = @json.map do |f|
|
22
|
+
os = parse_for_os(f['name'])
|
23
|
+
checksum = f['sha256']
|
24
|
+
[os, checksum]
|
25
|
+
end
|
26
|
+
@bottles = Hash[pairs]
|
27
|
+
end
|
28
|
+
|
29
|
+
# Formula -> Formula
|
30
|
+
#
|
31
|
+
# Put all bottles gathered here into the given formula, then return the result
|
32
|
+
def put_bottles_into(formula)
|
33
|
+
bottles.reduce(formula) do |formula, (os, checksum)|
|
34
|
+
formula.put_bottle(os, checksum)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
#private
|
39
|
+
|
40
|
+
# String -> String
|
41
|
+
#
|
42
|
+
# filename -> OS name
|
43
|
+
def parse_for_os(bottle_filename)
|
44
|
+
File.extname(
|
45
|
+
File.basename(bottle_filename, '.bottle.tar.gz')).
|
46
|
+
sub(/^\./, '')
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
@@ -0,0 +1,217 @@
|
|
1
|
+
|
2
|
+
require 'parser/current'
|
3
|
+
require 'unparser'
|
4
|
+
|
5
|
+
Parser::Builders::Default.emit_lambda = true
|
6
|
+
Parser::Builders::Default.emit_procarg0 = true
|
7
|
+
|
8
|
+
module HomebrewAutomation
|
9
|
+
|
10
|
+
# An internal representation of some Formula.rb Ruby source file, containing
|
11
|
+
# the definition of a Homebrew Bottle. See Homebrew docs for concepts:
|
12
|
+
# https://docs.brew.sh/Bottles.html
|
13
|
+
#
|
14
|
+
# Instance methods produce new instances where applicable, leaving all
|
15
|
+
# instances free from mutation.
|
16
|
+
class Formula
|
17
|
+
|
18
|
+
# A constructor method that parses the string form of a Homebrew Formula
|
19
|
+
# source file into an internal representation
|
20
|
+
#
|
21
|
+
# @return [Formula]
|
22
|
+
def self.parse_string s
|
23
|
+
Formula.new (Parser::CurrentRuby.parse s)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Take an post-parsing abstract syntax tree representation of a Homebrew Formula.
|
27
|
+
# This is mostly not intended for common use-cases.
|
28
|
+
#
|
29
|
+
# @param ast [Parser::AST::Node]
|
30
|
+
def initialize ast
|
31
|
+
@ast = ast
|
32
|
+
end
|
33
|
+
|
34
|
+
# Produce Homebrew Formula source code as a string, suitable for saving as
|
35
|
+
# a Ruby source file.
|
36
|
+
#
|
37
|
+
# @return [String]
|
38
|
+
def to_s
|
39
|
+
Unparser.unparse @ast
|
40
|
+
end
|
41
|
+
|
42
|
+
# Update a field in the Formula
|
43
|
+
#
|
44
|
+
# @param field [String] Name of the Formula field, e.g. `url`
|
45
|
+
# @param value [String] Value of the Formula field, e.g. `https://github.com/easoncxz/homebrew-automation`
|
46
|
+
# @return [Formula] a new instance of Formula with the changes applied
|
47
|
+
def update_field field, value
|
48
|
+
Formula.new update(
|
49
|
+
@ast,
|
50
|
+
[ by_type('begin'),
|
51
|
+
by_both(
|
52
|
+
by_type('send'),
|
53
|
+
by_msg(field)),
|
54
|
+
by_type('str')],
|
55
|
+
-> (n) { n.updated(nil, [value]) })
|
56
|
+
end
|
57
|
+
|
58
|
+
# Update two fields together
|
59
|
+
#
|
60
|
+
# @param url [String] URL of source tarball
|
61
|
+
# @param sha256 [String] SHA256 sum of source tarball
|
62
|
+
def put_sdist url, sha256
|
63
|
+
update_field("url", url).
|
64
|
+
update_field("sha256", sha256)
|
65
|
+
end
|
66
|
+
|
67
|
+
# Insert or replace the Homebrew Bottle for a given OS
|
68
|
+
#
|
69
|
+
# @param os [String] Operating system name, e.g. "yosemite", as per Homebrew's conventions
|
70
|
+
# @param sha256 [String] Checksum of the binary "Bottle" tarball
|
71
|
+
# @return [Formula] a new instance of Formula with the changes applied
|
72
|
+
def put_bottle os, sha256
|
73
|
+
Formula.new update(
|
74
|
+
@ast,
|
75
|
+
bot_begin_path,
|
76
|
+
put_bottle_version(os, sha256))
|
77
|
+
end
|
78
|
+
|
79
|
+
def == o
|
80
|
+
self.class == o.class && @ast == o.ast
|
81
|
+
end
|
82
|
+
|
83
|
+
def hash
|
84
|
+
@ast.hash
|
85
|
+
end
|
86
|
+
|
87
|
+
alias :eql? :==
|
88
|
+
|
89
|
+
protected
|
90
|
+
attr_reader :ast
|
91
|
+
|
92
|
+
private
|
93
|
+
|
94
|
+
# Path to the :begin node
|
95
|
+
# bot_begin_path :: [Choice]
|
96
|
+
# type Choice = Proc (Node -> Bool)
|
97
|
+
def bot_begin_path
|
98
|
+
[ by_type('begin'),
|
99
|
+
by_both(
|
100
|
+
by_type('block'),
|
101
|
+
by_child(
|
102
|
+
by_both(
|
103
|
+
by_type('send'),
|
104
|
+
by_msg('bottle')))),
|
105
|
+
by_type('begin')]
|
106
|
+
end
|
107
|
+
|
108
|
+
# Tricky: this is an insert-or-update
|
109
|
+
# put_bottle_version :: String -> String -> Proc (Node -> Node)
|
110
|
+
def put_bottle_version os, sha256
|
111
|
+
-> (bot_begin) {
|
112
|
+
bot_begin.updated(
|
113
|
+
nil, # keep the node type unchanged
|
114
|
+
bot_begin.children.reject(
|
115
|
+
# Get rid of any existing matching ones
|
116
|
+
&by_both(
|
117
|
+
by_msg('sha256'),
|
118
|
+
by_os(os))
|
119
|
+
# Then add the one we want
|
120
|
+
).push(new_sha256(sha256, os)))
|
121
|
+
}
|
122
|
+
end
|
123
|
+
|
124
|
+
# Build a new AST Node
|
125
|
+
# String -> String -> Node
|
126
|
+
def new_sha256 sha256, os
|
127
|
+
# Unparser doesn't like Sexp, so let's bring
|
128
|
+
# own own bit of "source code" inline.
|
129
|
+
sha256_send = Parser::CurrentRuby.parse(
|
130
|
+
'sha256 "checksum-here" => :some_os')
|
131
|
+
with_sha256 = update(
|
132
|
+
sha256_send,
|
133
|
+
[ by_type('hash'),
|
134
|
+
by_type('pair'),
|
135
|
+
by_type('str') ],
|
136
|
+
-> (n) { n.updated(nil, [sha256]) })
|
137
|
+
with_sha256_and_os = update(
|
138
|
+
with_sha256,
|
139
|
+
[ by_type('hash'),
|
140
|
+
by_type('pair'),
|
141
|
+
by_type('sym') ],
|
142
|
+
-> (n) { n.updated(nil, [os.to_sym]) })
|
143
|
+
with_sha256_and_os
|
144
|
+
end
|
145
|
+
|
146
|
+
# update :: Node -> [Choice] -> Proc (Node -> Node) -> Node
|
147
|
+
def update node, path, fn
|
148
|
+
if path.length == 0 then
|
149
|
+
fn.(node)
|
150
|
+
else
|
151
|
+
choose, *rest = path
|
152
|
+
node.updated(
|
153
|
+
nil, # Don't change node type
|
154
|
+
node.children.map do |c|
|
155
|
+
choose.(c) ? update(c, rest, fn) : c
|
156
|
+
end)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# zoom_in :: Node -> [Choice] -> Node
|
161
|
+
def zoom_in node, path
|
162
|
+
if path.length == 0 then
|
163
|
+
node
|
164
|
+
else
|
165
|
+
choose, *rest = path
|
166
|
+
chosen = node.children.select(&choose).first
|
167
|
+
zoom_in chosen, rest
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
# by_both
|
172
|
+
# :: Proc (Node -> Bool)
|
173
|
+
# -> Proc (Node -> Bool)
|
174
|
+
# -> Proc (Node -> Bool)
|
175
|
+
def by_both p, q
|
176
|
+
-> (n) { p.(n) && q.(n) }
|
177
|
+
end
|
178
|
+
|
179
|
+
# by_msg :: String -> Proc (Node -> Bool)
|
180
|
+
def by_msg msg
|
181
|
+
-> (n) { n.children[1] == msg.to_sym }
|
182
|
+
end
|
183
|
+
|
184
|
+
# by_type :: String -> Proc (Node -> Bool)
|
185
|
+
def by_type type
|
186
|
+
-> (n) {
|
187
|
+
n &&
|
188
|
+
n.is_a?(AST::Node) &&
|
189
|
+
n.type == type.to_sym
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
# Matches if one of the node's children matches the given p
|
194
|
+
# by_child :: Proc (Node -> Bool) -> Proc (Node -> Bool)
|
195
|
+
def by_child p
|
196
|
+
-> (n) {
|
197
|
+
n &&
|
198
|
+
n.is_a?(AST::Node) &&
|
199
|
+
n.children.select(&p).size > 0
|
200
|
+
}
|
201
|
+
end
|
202
|
+
|
203
|
+
# Matches if this :send node expresses the give sha256 sum
|
204
|
+
# by_os :: String -> Proc (Node -> Bool)
|
205
|
+
def by_os os
|
206
|
+
-> (n) {
|
207
|
+
zoom_in(n, [
|
208
|
+
by_type('hash'),
|
209
|
+
by_type('pair'),
|
210
|
+
by_type('sym')])
|
211
|
+
.children[0] == os.to_sym
|
212
|
+
}
|
213
|
+
end
|
214
|
+
|
215
|
+
end
|
216
|
+
|
217
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
|
2
|
+
module HomebrewAutomation
|
3
|
+
|
4
|
+
class MacOS
|
5
|
+
|
6
|
+
# () -> String
|
7
|
+
#
|
8
|
+
# Returns a representation of the macOS version name in a format recognised by Homebrew,
|
9
|
+
# in particular the Formula/Bottle DSL.
|
10
|
+
def self.identify_version
|
11
|
+
version = `sw_vers -productVersion`
|
12
|
+
mac_to_homebrew.
|
13
|
+
select { |pattern, _| pattern === version }.
|
14
|
+
map { |_, description| description }.
|
15
|
+
first
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.mac_to_homebrew
|
19
|
+
{
|
20
|
+
/^10.10/ => 'yosemite',
|
21
|
+
/^10.11/ => 'el_capitan',
|
22
|
+
/^10.12/ => 'sierra',
|
23
|
+
/^10.13/ => 'high_sierra'
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
|
2
|
+
require 'http'
|
3
|
+
|
4
|
+
module HomebrewAutomation
|
5
|
+
|
6
|
+
# A representation of a source distribution tarball file
|
7
|
+
class SourceDist
|
8
|
+
|
9
|
+
# @param tag [String] a Git tag, e.g. "v0.1.1.14"
|
10
|
+
def initialize user, repo, tag
|
11
|
+
@user = user
|
12
|
+
@repo = repo
|
13
|
+
@tag = tag
|
14
|
+
end
|
15
|
+
|
16
|
+
attr_reader :user, :repo, :tag
|
17
|
+
|
18
|
+
# Calculate and return the file's checksum. Lazy and memoized.
|
19
|
+
#
|
20
|
+
# @return [String] hex-encoded string representation of the checksum
|
21
|
+
def sha256
|
22
|
+
@sha256 ||= Digest::SHA256.hexdigest contents
|
23
|
+
end
|
24
|
+
|
25
|
+
# Fetch the file contents over HTTP. Lazy and memoized.
|
26
|
+
#
|
27
|
+
# @param fake [String] fake file contents (for testing)
|
28
|
+
# @return [String] contents of the file
|
29
|
+
def contents fake: nil
|
30
|
+
@contents = @contents || fake ||
|
31
|
+
begin
|
32
|
+
resp = HTTP.follow.get url
|
33
|
+
case resp.code
|
34
|
+
when 200
|
35
|
+
resp.body.to_s
|
36
|
+
else
|
37
|
+
puts resp
|
38
|
+
raise StandardError.new resp.code
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Pure
|
44
|
+
#
|
45
|
+
# @return [String]
|
46
|
+
def url
|
47
|
+
"https://github.com/#{@user}/#{@repo}/archive/#{@tag}.tar.gz"
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
|
2
|
+
require_relative "formula.rb"
|
3
|
+
|
4
|
+
module HomebrewAutomation
|
5
|
+
|
6
|
+
class Tap
|
7
|
+
|
8
|
+
# Get a token from: https://github.com/settings/tokens
|
9
|
+
#
|
10
|
+
# @param keep_submodule [Boolean] When done, don't delete the tap Git repo
|
11
|
+
def initialize(user, repo, token, keep_submodule: false)
|
12
|
+
@repo = repo
|
13
|
+
@url = "https://#{token}@github.com/#{user}/#{repo}.git"
|
14
|
+
@keep_submodule = keep_submodule
|
15
|
+
end
|
16
|
+
|
17
|
+
attr_reader :user, :repo, :token
|
18
|
+
|
19
|
+
# forall a. Block (() -> a) -> a
|
20
|
+
#
|
21
|
+
# Do something in a fresh clone, then clean-up.
|
22
|
+
def with_git_clone(&block)
|
23
|
+
begin
|
24
|
+
git_clone
|
25
|
+
Dir.chdir @repo, &block
|
26
|
+
ensure
|
27
|
+
remove_git_submodule unless @keep_submodule
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# (String, Block (Formula -> Formula)) -> nil
|
32
|
+
#
|
33
|
+
# Overwrite the given formula
|
34
|
+
def on_formula(formula, &block)
|
35
|
+
name = "#{formula}.rb"
|
36
|
+
block ||= ->(n) { n }
|
37
|
+
Dir.chdir 'Formula' do
|
38
|
+
File.open name, 'r' do |old_file|
|
39
|
+
File.open "#{name}.new", 'w' do |new_file|
|
40
|
+
new_file.write(
|
41
|
+
block.
|
42
|
+
call(Formula.parse_string(old_file.read)).
|
43
|
+
to_s)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
File.rename "#{name}.new", name
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def git_config
|
51
|
+
name = ENV['TRAVIS_GIT_USER_NAME']
|
52
|
+
email = ENV['TRAVIS_GIT_USER_EMAIL']
|
53
|
+
if name && email
|
54
|
+
system 'git', 'config', '--global', 'user.name', name
|
55
|
+
system 'git', 'config', '--global', 'user.email', email
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def git_commit_am(msg)
|
60
|
+
die unless system "git", "commit", "-am", msg
|
61
|
+
end
|
62
|
+
|
63
|
+
def git_push
|
64
|
+
die unless system "git", "push"
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
#private
|
69
|
+
|
70
|
+
|
71
|
+
def git_clone
|
72
|
+
die unless system "git", "clone", @url
|
73
|
+
end
|
74
|
+
|
75
|
+
def remove_git_submodule
|
76
|
+
die unless system "rm", "-rf", @repo
|
77
|
+
end
|
78
|
+
|
79
|
+
def die
|
80
|
+
raise StandardError.new
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
|
2
|
+
require_relative './mac-os.rb'
|
3
|
+
require_relative './bottle_gatherer.rb'
|
4
|
+
|
5
|
+
module HomebrewAutomation
|
6
|
+
|
7
|
+
# Imperative glue code
|
8
|
+
#
|
9
|
+
# Probably each method suits to become a CLI command.
|
10
|
+
class Workflow
|
11
|
+
|
12
|
+
# @param tap [HomebrewAutomation::Tap]
|
13
|
+
# @param bintray [HomebrewAutomation::Bintray]
|
14
|
+
def initialize(
|
15
|
+
tap,
|
16
|
+
bintray,
|
17
|
+
bintray_bottle_repo: 'homebrew-bottles')
|
18
|
+
@tap = tap
|
19
|
+
@bintray = bintray
|
20
|
+
@bintray_bottle_repo = bintray_bottle_repo
|
21
|
+
end
|
22
|
+
|
23
|
+
# Build a bottle from the given source tarball reference
|
24
|
+
#
|
25
|
+
# @param source_dist [HomebrewAutomation::SourceDist] Source tarball
|
26
|
+
# @param formula_name [String] Formula name as appears in the Tap, which should be the same as the Bintray "Package" name
|
27
|
+
# @param version_name [String] Bintray package "Version" name; defaults to stripping leading `v` from the Git tag.
|
28
|
+
# @return [Bottle]
|
29
|
+
def build_and_upload_bottle(source_dist, formula_name: nil, version_name: nil)
|
30
|
+
formula_name ||= source_dist.repo
|
31
|
+
version_name ||= source_dist.tag.sub(/^v/, '')
|
32
|
+
os_name = MacOS.identify_version
|
33
|
+
@tap.with_git_clone do
|
34
|
+
@tap.on_formula(formula_name) do |formula|
|
35
|
+
formula.put_sdist(source_dist.url, source_dist.sha256)
|
36
|
+
end
|
37
|
+
@tap.git_commit_am "Throwaway commit; just for building bottles"
|
38
|
+
|
39
|
+
local_tap_url = File.realpath('.')
|
40
|
+
bottle = Bottle.new(local_tap_url, formula_name, os_name)
|
41
|
+
bottle.build
|
42
|
+
|
43
|
+
@bintray.create_version(
|
44
|
+
@bintray_bottle_repo,
|
45
|
+
formula_name,
|
46
|
+
version_name)
|
47
|
+
@bintray.upload_file(
|
48
|
+
@bintray_bottle_repo,
|
49
|
+
formula_name,
|
50
|
+
version_name,
|
51
|
+
bottle.filename,
|
52
|
+
bottle.content)
|
53
|
+
|
54
|
+
bottle
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
# Look around on Bintray to see what bottles we've previously built, then
|
59
|
+
# push new commits into the Tap repository to register the new bottles.
|
60
|
+
#
|
61
|
+
# @param formula_name [String]
|
62
|
+
# @param version_name [String] Bintray "Version" name, not a Git tag
|
63
|
+
# @return [Formula]
|
64
|
+
def gather_and_publish_bottles(formula_name, version_name)
|
65
|
+
@tap.with_git_clone do
|
66
|
+
resp = @bintray.get_all_files_in_version(
|
67
|
+
@bintray_bottle_repo,
|
68
|
+
formula_name,
|
69
|
+
version_name)
|
70
|
+
unless (200..207) === resp.code
|
71
|
+
puts resp
|
72
|
+
raise StandardError.new(resp)
|
73
|
+
end
|
74
|
+
|
75
|
+
json = JSON.parse(resp.body)
|
76
|
+
gatherer = BottleGatherer.new(json)
|
77
|
+
|
78
|
+
@tap.on_formula(formula_name) do |formula|
|
79
|
+
gatherer.put_bottles_into(formula)
|
80
|
+
end
|
81
|
+
|
82
|
+
@tap.git_config
|
83
|
+
@tap.git_commit_am "Add bottles for #{formula_name}@#{version_name}"
|
84
|
+
@tap.git_push
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
data/lib/homebrew_automation.rb
CHANGED
@@ -1,172 +1,14 @@
|
|
1
1
|
|
2
|
-
|
3
|
-
require 'unparser'
|
4
|
-
|
5
|
-
Parser::Builders::Default.emit_lambda = true
|
6
|
-
Parser::Builders::Default.emit_procarg0 = true
|
7
|
-
|
2
|
+
# Helps you manipulate Homebrew Formula files, Bottles etc.
|
8
3
|
module HomebrewAutomation
|
9
|
-
|
10
|
-
class Formula
|
11
|
-
|
12
|
-
# Formula::parse_string :: String -> Formula
|
13
|
-
def self.parse_string s
|
14
|
-
Formula.new (Parser::CurrentRuby.parse s)
|
15
|
-
end
|
16
|
-
|
17
|
-
# Formula::new :: Parser::AST::Node -> Formula
|
18
|
-
def initialize ast
|
19
|
-
@ast = ast
|
20
|
-
end
|
21
|
-
|
22
|
-
def to_s
|
23
|
-
Unparser.unparse @ast
|
24
|
-
end
|
25
|
-
|
26
|
-
# update_field :: String -> String -> Formula
|
27
|
-
def update_field field, value
|
28
|
-
Formula.new update(
|
29
|
-
@ast,
|
30
|
-
[ by_type('begin'),
|
31
|
-
by_both(
|
32
|
-
by_type('send'),
|
33
|
-
by_msg(field)),
|
34
|
-
by_type('str')],
|
35
|
-
-> (n) { n.updated(nil, [value]) })
|
36
|
-
end
|
37
|
-
|
38
|
-
# Insert or replace the bottle for a given OS
|
39
|
-
# put_bottle :: String -> String -> Node -> Node
|
40
|
-
def put_bottle os, sha256
|
41
|
-
Formula.new update(
|
42
|
-
@ast,
|
43
|
-
bot_begin_path,
|
44
|
-
put_bottle_version(os, sha256))
|
45
|
-
end
|
46
|
-
|
47
|
-
private
|
48
|
-
|
49
|
-
# Path to the :begin node
|
50
|
-
# bot_begin_path :: [Choice]
|
51
|
-
# type Choice = Proc (Node -> Bool)
|
52
|
-
def bot_begin_path
|
53
|
-
[ by_type('begin'),
|
54
|
-
by_both(
|
55
|
-
by_type('block'),
|
56
|
-
by_child(
|
57
|
-
by_both(
|
58
|
-
by_type('send'),
|
59
|
-
by_msg('bottle')))),
|
60
|
-
by_type('begin')]
|
61
|
-
end
|
62
|
-
|
63
|
-
# Tricky: this is an insert-or-update
|
64
|
-
# put_bottle_version :: String -> String -> Proc (Node -> Node)
|
65
|
-
def put_bottle_version os, sha256
|
66
|
-
-> (bot_begin) {
|
67
|
-
bot_begin.updated(
|
68
|
-
nil, # keep the node type the unchanged
|
69
|
-
bot_begin.children.reject(
|
70
|
-
# Get rid of any existing matching ones
|
71
|
-
&by_both(
|
72
|
-
by_msg('sha256'),
|
73
|
-
by_os(os))
|
74
|
-
# Then add the one we want
|
75
|
-
).push(new_sha256(sha256, os)))
|
76
|
-
}
|
77
|
-
end
|
78
|
-
|
79
|
-
# Build a new AST Node
|
80
|
-
# String -> String -> Node
|
81
|
-
def new_sha256 sha256, os
|
82
|
-
# Unparser doesn't like Sexp, so let's bring
|
83
|
-
# own own bit of "source code" inline.
|
84
|
-
sha256_send = Parser::CurrentRuby.parse(
|
85
|
-
'sha256 "checksum-here" => :some_os')
|
86
|
-
with_sha256 = update(
|
87
|
-
sha256_send,
|
88
|
-
[ by_type('hash'),
|
89
|
-
by_type('pair'),
|
90
|
-
by_type('str') ],
|
91
|
-
-> (n) { n.updated(nil, [sha256]) })
|
92
|
-
with_sha256_and_os = update(
|
93
|
-
with_sha256,
|
94
|
-
[ by_type('hash'),
|
95
|
-
by_type('pair'),
|
96
|
-
by_type('sym') ],
|
97
|
-
-> (n) { n.updated(nil, [os.to_sym]) })
|
98
|
-
with_sha256_and_os
|
99
|
-
end
|
100
|
-
|
101
|
-
# update :: Node -> [Choice] -> Proc (Node -> Node) -> Node
|
102
|
-
def update node, path, fn
|
103
|
-
if path.length == 0 then
|
104
|
-
fn.(node)
|
105
|
-
else
|
106
|
-
choose, *rest = path
|
107
|
-
node.updated(
|
108
|
-
nil, # Don't change node type
|
109
|
-
node.children.map do |c|
|
110
|
-
choose.(c) ? update(c, rest, fn) : c
|
111
|
-
end)
|
112
|
-
end
|
113
|
-
end
|
114
|
-
|
115
|
-
# zoom_in :: Node -> [Choice] -> Node
|
116
|
-
def zoom_in node, path
|
117
|
-
if path.length == 0 then
|
118
|
-
node
|
119
|
-
else
|
120
|
-
choose, *rest = path
|
121
|
-
chosen = node.children.select(&choose).first
|
122
|
-
zoom_in chosen, rest
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
# by_both
|
127
|
-
# :: Proc (Node -> Bool)
|
128
|
-
# -> Proc (Node -> Bool)
|
129
|
-
# -> Proc (Node -> Bool)
|
130
|
-
def by_both p, q
|
131
|
-
-> (n) { p.(n) && q.(n) }
|
132
|
-
end
|
133
|
-
|
134
|
-
# by_msg :: String -> Proc (Node -> Bool)
|
135
|
-
def by_msg msg
|
136
|
-
-> (n) { n.children[1] == msg.to_sym }
|
137
|
-
end
|
138
|
-
|
139
|
-
# by_type :: String -> Proc (Node -> Bool)
|
140
|
-
def by_type type
|
141
|
-
-> (n) {
|
142
|
-
n &&
|
143
|
-
n.is_a?(AST::Node) &&
|
144
|
-
n.type == type.to_sym
|
145
|
-
}
|
146
|
-
end
|
147
|
-
|
148
|
-
# Matches if one of the node's children matches the given p
|
149
|
-
# by_child :: Proc (Node -> Bool) -> Proc (Node -> Bool)
|
150
|
-
def by_child p
|
151
|
-
-> (n) {
|
152
|
-
n &&
|
153
|
-
n.is_a?(AST::Node) &&
|
154
|
-
n.children.select(&p).size > 0
|
155
|
-
}
|
156
|
-
end
|
157
|
-
|
158
|
-
# Matches if this :send node expresses the give sha256 sum
|
159
|
-
# by_os :: String -> Proc (Node -> Bool)
|
160
|
-
def by_os os
|
161
|
-
-> (n) {
|
162
|
-
zoom_in(n, [
|
163
|
-
by_type('hash'),
|
164
|
-
by_type('pair'),
|
165
|
-
by_type('sym')])
|
166
|
-
.children[0] == os.to_sym
|
167
|
-
}
|
168
|
-
end
|
169
|
-
|
170
|
-
end
|
171
|
-
|
172
4
|
end
|
5
|
+
|
6
|
+
require_relative 'homebrew_automation/bintray.rb'
|
7
|
+
require_relative 'homebrew_automation/bottle.rb'
|
8
|
+
require_relative 'homebrew_automation/bottle_gatherer.rb'
|
9
|
+
require_relative 'homebrew_automation/formula.rb'
|
10
|
+
require_relative 'homebrew_automation/mac-os.rb'
|
11
|
+
require_relative 'homebrew_automation/source_dist.rb'
|
12
|
+
require_relative 'homebrew_automation/tap.rb'
|
13
|
+
require_relative 'homebrew_automation/version.rb'
|
14
|
+
require_relative 'homebrew_automation/workflow.rb'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: homebrew_automation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.8
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- easoncxz
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-10-08 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '12.3'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: pry-byebug
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.6'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.6'
|
41
55
|
- !ruby/object:Gem::Dependency
|
42
56
|
name: thor
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -52,6 +66,20 @@ dependencies:
|
|
52
66
|
- - "~>"
|
53
67
|
- !ruby/object:Gem::Version
|
54
68
|
version: '0.20'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: http
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '3'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '3'
|
55
83
|
- !ruby/object:Gem::Dependency
|
56
84
|
name: parser
|
57
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,10 +108,21 @@ dependencies:
|
|
80
108
|
- - "~>"
|
81
109
|
- !ruby/object:Gem::Version
|
82
110
|
version: '0.2'
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: rest-client
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '2.0'
|
118
|
+
type: :runtime
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '2.0'
|
125
|
+
description: Build Bottles and update Formulae. Please see README on Github for details.
|
87
126
|
email: me@easoncxz.com
|
88
127
|
executables:
|
89
128
|
- homebrew_automation.rb
|
@@ -92,7 +131,15 @@ extra_rdoc_files: []
|
|
92
131
|
files:
|
93
132
|
- bin/homebrew_automation.rb
|
94
133
|
- lib/homebrew_automation.rb
|
134
|
+
- lib/homebrew_automation/bintray.rb
|
135
|
+
- lib/homebrew_automation/bottle.rb
|
136
|
+
- lib/homebrew_automation/bottle_gatherer.rb
|
137
|
+
- lib/homebrew_automation/formula.rb
|
138
|
+
- lib/homebrew_automation/mac-os.rb
|
139
|
+
- lib/homebrew_automation/source_dist.rb
|
140
|
+
- lib/homebrew_automation/tap.rb
|
95
141
|
- lib/homebrew_automation/version.rb
|
142
|
+
- lib/homebrew_automation/workflow.rb
|
96
143
|
homepage: https://github.com/easoncxz/homebrew-automation
|
97
144
|
licenses:
|
98
145
|
- GPL-3.0
|
@@ -113,8 +160,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
160
|
version: '0'
|
114
161
|
requirements: []
|
115
162
|
rubyforge_project:
|
116
|
-
rubygems_version: 2.7.
|
163
|
+
rubygems_version: 2.7.7
|
117
164
|
signing_key:
|
118
165
|
specification_version: 4
|
119
|
-
summary:
|
166
|
+
summary: Build bottles and update Formulae
|
120
167
|
test_files: []
|