mirror of
https://github.com/jimeh/build-emacs-for-macos.git
synced 2026-02-19 13:06:38 +00:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
228ae0939c
|
|||
|
ac943c430c
|
|||
|
bc3923c9ca
|
|||
|
e6b1e5a554
|
|||
|
94625fce38
|
|||
|
b03343f506
|
|||
|
b1896d4a4f
|
|||
|
9d32509c61
|
|||
|
1c2745cd36
|
|||
|
80a0d55b24
|
|||
|
fd0ec4d772
|
|||
|
c0c809a86a
|
|||
|
cb63806262
|
|||
|
6d7ab95ca2
|
|||
|
f4d6e3a56d
|
|||
|
3f1059940d
|
|||
|
5c722e36c5
|
|||
|
f52dd8dc6d
|
|||
|
4cdbaf1ec0
|
|||
|
f3a289b11c
|
|||
|
9019e73d60
|
|||
|
28930381a8
|
|||
|
df25e54ef7
|
|||
|
6d21d1bef4
|
|||
|
99aa76b398
|
|||
|
b60ca528f8
|
|||
|
23b8236e0a
|
|||
|
56d0364099
|
|||
|
6af597b427
|
|||
|
a331457e89
|
|||
| a4171555f5 | |||
|
adbcfc6fc4
|
93
CHANGELOG.md
93
CHANGELOG.md
@@ -2,6 +2,99 @@
|
||||
|
||||
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
|
||||
|
||||
### [0.6.13](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.12...v0.6.13) (2021-08-04)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **native_comp:** add option to enable/disable relinking *.eln files ([ac943c4](https://github.com/jimeh/build-emacs-for-macos/commit/ac943c430c58e0761ac44e8d25d4d55a461d01a2))
|
||||
|
||||
### [0.6.12](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.11...v0.6.12) (2021-08-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **sign:** resolve signing issue caused by re-linking shared lib in *.eln files ([e6b1e5a](https://github.com/jimeh/build-emacs-for-macos/commit/e6b1e5a554fd0f776bd01c17cfb1ebbbdf7a7831))
|
||||
|
||||
### [0.6.11](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.10...v0.6.11) (2021-07-17)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **native-comp:** fix re-linking and signing issue with *.eln files ([b03343f](https://github.com/jimeh/build-emacs-for-macos/commit/b03343f506aa3ceabdfa03f8a2916b2db4873f3f))
|
||||
|
||||
### [0.6.10](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.9...v0.6.10) (2021-07-16)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **native-comp:** *.eln files were not being found during shared lib embedding ([9d32509](https://github.com/jimeh/build-emacs-for-macos/commit/9d32509c615076618957cc47c82f6e9d8f972fe7))
|
||||
|
||||
### [0.6.9](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.8...v0.6.9) (2021-07-04)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **release:** add bulk edit command to quickly change multiple GitHub releases ([cb63806](https://github.com/jimeh/build-emacs-for-macos/commit/cb638062625d9bc3eee12515067fb09e05a08414))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **plan:** correctly parse --test-release-type flag ([fd0ec4d](https://github.com/jimeh/build-emacs-for-macos/commit/fd0ec4d772dd3da93afc234fb3024220b2099c88))
|
||||
|
||||
### [0.6.8](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.7...v0.6.8) (2021-07-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **builds:** add support for stable builds ([f4d6e3a](https://github.com/jimeh/build-emacs-for-macos/commit/f4d6e3a56d2c15b0c86af18e8d16bebbeb92a8ab))
|
||||
|
||||
### [0.6.7](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.6...v0.6.7) (2021-07-02)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **bundle:** move bundled shared libraries to Contents/Frameworks ([5c722e3](https://github.com/jimeh/build-emacs-for-macos/commit/5c722e36c571aa7bf558b7f210c011f12d8d8a1c))
|
||||
|
||||
### [0.6.6](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.5...v0.6.6) (2021-07-01)
|
||||
|
||||
### [0.6.5](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.4...v0.6.5) (2021-07-01)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **native_comp:** improve handling of *.eln files in .app bundle ([9019e73](https://github.com/jimeh/build-emacs-for-macos/commit/9019e73d606f0379f988f46d6008770f8f3f7a51))
|
||||
|
||||
### [0.6.4](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.3...v0.6.4) (2021-06-30)
|
||||
|
||||
### [0.6.3](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.2...v0.6.3) (2021-06-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **patches:** correctly set ref when loading a build plan YAML ([99aa76b](https://github.com/jimeh/build-emacs-for-macos/commit/99aa76b3985195c310a20bafa19a8c7a4c8558fd))
|
||||
|
||||
### [0.6.2](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.1...v0.6.2) (2021-06-29)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **native_comp:** patch Emacs.pdmp for customized native-lisp paths ([23b8236](https://github.com/jimeh/build-emacs-for-macos/commit/23b8236e0a66fb09810e8422bedf02f7192a53e4))
|
||||
|
||||
### [0.6.1](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.0...v0.6.1) (2021-06-28)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **cask:** add missing --force flag to cask update command ([6af597b](https://github.com/jimeh/build-emacs-for-macos/commit/6af597b4271341f9796c3d9c356de9918e0f6f85))
|
||||
|
||||
## [0.6.0](https://github.com/jimeh/build-emacs-for-macos/compare/v0.5.2...v0.6.0) (2021-06-28)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **cask:** add cask update command to manage cask formula ([adbcfc6](https://github.com/jimeh/build-emacs-for-macos/commit/adbcfc6fc433fcc99b10dc5ccb51ba458333fa9c))
|
||||
|
||||
### [0.5.2](https://github.com/jimeh/build-emacs-for-macos/compare/v0.5.1...v0.5.2) (2021-06-27)
|
||||
|
||||
|
||||
|
||||
@@ -45,6 +45,11 @@ The build produced does have some limitations:
|
||||
that built the application will yield warnings. If you want to make a signed
|
||||
Emacs.app, google is you friend for finding signing instructions.
|
||||
|
||||
## Binary Builds
|
||||
|
||||
Nightly and stable binary builds produced with this build script are available
|
||||
from [jimeh/emacs-builds](https://github.com/jimeh/emacs-builds).
|
||||
|
||||
## Requirements
|
||||
|
||||
- [Xcode](https://apps.apple.com/gb/app/xcode/id497799835?mt=12)
|
||||
|
||||
@@ -11,6 +11,7 @@ require 'net/http'
|
||||
require 'optparse'
|
||||
require 'pathname'
|
||||
require 'time'
|
||||
require 'tmpdir'
|
||||
require 'uri'
|
||||
require 'yaml'
|
||||
|
||||
@@ -106,7 +107,7 @@ class Build
|
||||
handle_native_lisp(app)
|
||||
add_cli_helper(app)
|
||||
|
||||
LibEmbedder.new(app, brew_dir, extra_libs).embed
|
||||
LibEmbedder.new(app, brew_dir, extra_libs, optsion[:relink_eln]).embed
|
||||
GccLibEmbedder.new(app, gcc_info).embed if options[:native_comp]
|
||||
|
||||
archive_build(build_dir) if options[:archive]
|
||||
@@ -117,9 +118,10 @@ class Build
|
||||
def load_plan(filename)
|
||||
plan = YAML.safe_load(File.read(filename), [:Time])
|
||||
|
||||
@ref = plan.dig('source', 'ref')
|
||||
@meta = {
|
||||
sha: plan.dig('source', 'commit', 'sha'),
|
||||
ref: plan.dig('source', 'ref'),
|
||||
ref: @ref,
|
||||
date: plan.dig('source', 'commit', 'date')
|
||||
}
|
||||
|
||||
@@ -155,11 +157,22 @@ class Build
|
||||
end
|
||||
|
||||
def extra_libs
|
||||
@extra_libs ||= [
|
||||
return @extra_libs if @extra_libs
|
||||
|
||||
libs = [
|
||||
File.join(brew_dir, 'opt/expat/lib/libexpat.1.dylib'),
|
||||
File.join(brew_dir, 'opt/libiconv/lib/libiconv.2.dylib'),
|
||||
File.join(brew_dir, 'opt/zlib/lib/libz.1.dylib')
|
||||
]
|
||||
|
||||
if options[:native_comp]
|
||||
libgcc_s = File.join(
|
||||
brew_dir, 'lib', 'gcc', gcc_info.major_version, 'libgcc_s.1.dylib'
|
||||
)
|
||||
libs << libgcc_s if File.exist?(libgcc_s)
|
||||
end
|
||||
|
||||
@extra_libs = libs
|
||||
end
|
||||
|
||||
def download_tarball(sha)
|
||||
@@ -292,6 +305,8 @@ class Build
|
||||
"-L#{gcc_info.libgccjit_lib_dir}",
|
||||
"-I#{File.join(gcc_info.root_dir, 'include')}",
|
||||
"-I#{File.join(gcc_info.libgccjit_root_dir, 'include')}",
|
||||
# Ensure library re-linking and code signing will work after building.
|
||||
'-Wl,-headerpad_max_install_names',
|
||||
ENV['LDFLAGS']
|
||||
].compact.join(' ')
|
||||
|
||||
@@ -411,20 +426,17 @@ class Build
|
||||
contents_dir = File.join(app, 'Contents')
|
||||
|
||||
FileUtils.cd(contents_dir) do
|
||||
# Skip creation of symlinks if *.eln files are located under
|
||||
# Resources/native-lisp. Emacs is capable of finding lisp sources and
|
||||
# *.eln cache files without symlinks.
|
||||
return if Dir['Resources/native-lisp/**/*.eln'].any?
|
||||
|
||||
info 'Creating symlinks within Emacs.app needed for native-comp'
|
||||
|
||||
FileUtils.ln_s('Resources/lisp', 'lisp') unless File.exist?('lisp')
|
||||
|
||||
source = Dir['MacOS/libexec/emacs/**/eln-cache',
|
||||
'MacOS/lib/emacs/**/native-lisp'].first
|
||||
|
||||
if source.nil?
|
||||
err 'Failed to find native-lisp cache directory for symlink creation.'
|
||||
# Skip creation of symlinks if *.eln files are not located in a location
|
||||
# known to be used by builds which need symlinks and other tweaks.
|
||||
return if source.nil?
|
||||
|
||||
info 'Creating symlinks within Emacs.app needed for native-comp'
|
||||
|
||||
if !File.exist?('lisp') && File.exist?('Resources/lisp')
|
||||
FileUtils.ln_s('Resources/lisp', 'lisp')
|
||||
end
|
||||
|
||||
# Check for folder name containing two dots (.), as this causes Apple's
|
||||
@@ -434,10 +446,9 @@ class Build
|
||||
# The workaround for now is to rename the folder replacing the dots with
|
||||
# hyphens (-), and create the native-lisp symlink pointing to the new
|
||||
# location.
|
||||
if source.match(%r{/.+\..+\..+/})
|
||||
# Dig deeper into native-lisp directory
|
||||
eln_dir = File.dirname(Dir[File.join(source, '**', '*.eln')].first)
|
||||
eln_dir = File.dirname(Dir[File.join(source, '**', '*.eln')].first)
|
||||
|
||||
if eln_dir.match(%r{/.+\..+\..+/})
|
||||
base = File.basename(eln_dir)
|
||||
parent = File.dirname(eln_dir)
|
||||
|
||||
@@ -454,6 +465,13 @@ class Build
|
||||
parent = File.dirname(parent)
|
||||
end
|
||||
|
||||
eln_parts = eln_dir.match(
|
||||
%r{/(\d+\.\d+\.\d+)/native-lisp/(\d+\.\d+\.\d+-\w+)(?:/.+)?$}i
|
||||
)
|
||||
if eln_parts
|
||||
patch_dump_native_lisp_paths(app, eln_parts[1], eln_parts[2])
|
||||
end
|
||||
|
||||
# Find native-lisp directory again after it has been renamed.
|
||||
source = Dir['MacOS/libexec/emacs/**/eln-cache',
|
||||
'MacOS/lib/emacs/**/native-lisp'].first
|
||||
@@ -468,6 +486,29 @@ class Build
|
||||
end
|
||||
end
|
||||
|
||||
def patch_dump_native_lisp_paths(app, emacs_version, eln_version)
|
||||
sanitized_emacs_version = emacs_version.gsub('.', '-')
|
||||
sanitized_eln_version = eln_version.gsub('.', '-')
|
||||
|
||||
contents_dir = File.join(app, 'Contents')
|
||||
FileUtils.cd(contents_dir) do
|
||||
filename = Dir['MacOS/Emacs.pdmp', 'MacOS/libexec/Emacs.pdmp'].first
|
||||
err "no Emacs.pdmp file found in #{app}" unless filename
|
||||
info 'patching Emacs.pdmp to point at new native-lisp paths'
|
||||
|
||||
content = File.read(filename, mode: 'rb').gsub(
|
||||
"lib/emacs/#{emacs_version}/native-lisp/#{eln_version}/",
|
||||
"lib/emacs/#{sanitized_emacs_version}/" \
|
||||
"native-lisp/#{sanitized_eln_version}/"
|
||||
).gsub(
|
||||
"../native-lisp/#{eln_version}/",
|
||||
"../native-lisp/#{sanitized_eln_version}/"
|
||||
)
|
||||
|
||||
File.open(filename, 'w') { |f| f.write(content) }
|
||||
end
|
||||
end
|
||||
|
||||
def add_cli_helper(app)
|
||||
source = File.join(__dir__, 'helper', 'emacs-cli.bash')
|
||||
target = File.join(app, 'Contents', 'MacOS', 'bin', 'emacs')
|
||||
@@ -708,8 +749,6 @@ class AbstractEmbedder
|
||||
@app = app
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def invocation_dir
|
||||
File.join(app, 'Contents', 'MacOS')
|
||||
end
|
||||
@@ -727,7 +766,7 @@ class LibEmbedder < AbstractEmbedder
|
||||
attr_reader :lib_source
|
||||
attr_reader :extra_libs
|
||||
|
||||
def initialize(app, lib_source, extra_libs = [])
|
||||
def initialize(app, lib_source, extra_libs = [], embed_eln_files = true)
|
||||
super(app)
|
||||
|
||||
@lib_source = lib_source
|
||||
@@ -749,7 +788,7 @@ class LibEmbedder < AbstractEmbedder
|
||||
rel_path = Pathname.new(lib_dir).relative_path_from(
|
||||
Pathname.new(File.dirname(binary))
|
||||
).to_s
|
||||
eln_files.each { |f| copy_libs(f, rel_path) }
|
||||
eln_files.each { |f| copy_libs(f, rel_path) } if embed_eln_files
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -757,19 +796,7 @@ class LibEmbedder < AbstractEmbedder
|
||||
private
|
||||
|
||||
def eln_files
|
||||
@eln_files ||= Dir[
|
||||
File.join(
|
||||
app, 'Contents', 'Resources', 'native-lisp', '**', '*.eln'
|
||||
),
|
||||
File.join(
|
||||
app, 'Contents', 'MacOS', 'lib', 'emacs', '**',
|
||||
'native-lisp', '**', '*.eln'
|
||||
),
|
||||
File.join(
|
||||
app, 'Contents', 'MacOS', 'libexec', 'emacs', '**',
|
||||
'eln-cache', '**', '*.eln'
|
||||
)
|
||||
]
|
||||
@eln_files ||= Dir[File.join(app, 'Contents', '**', '*.eln')]
|
||||
end
|
||||
|
||||
def copy_libs(exe, rel_path = nil)
|
||||
@@ -875,15 +902,15 @@ class GccLibEmbedder < AbstractEmbedder
|
||||
end
|
||||
|
||||
def target_dir
|
||||
File.join(invocation_dir, gcc_info.relative_lib_dir)
|
||||
File.join(lib_dir, gcc_info.relative_lib_dir)
|
||||
end
|
||||
|
||||
def source_darwin_dir
|
||||
File.join(invocation_dir, gcc_info.relative_darwin_lib_dir)
|
||||
File.join(lib_dir, gcc_info.relative_darwin_lib_dir)
|
||||
end
|
||||
|
||||
def target_darwin_dir
|
||||
File.join(invocation_dir, gcc_info.sanitized_relative_darwin_lib_dir)
|
||||
File.join(lib_dir, gcc_info.sanitized_relative_darwin_lib_dir)
|
||||
end
|
||||
|
||||
def source_dir
|
||||
@@ -939,7 +966,21 @@ class GccInfo
|
||||
def sanitized_relative_darwin_lib_dir
|
||||
@sanitized_relative_darwin_lib_dir ||= File.join(
|
||||
File.dirname(relative_darwin_lib_dir),
|
||||
File.basename(relative_darwin_lib_dir).split('.').first
|
||||
File.basename(relative_darwin_lib_dir).gsub('.', '_')
|
||||
)
|
||||
end
|
||||
|
||||
def app_bundle_relative_lib_dir
|
||||
@app_bundle_relative_lib_dir ||= relative_dir(
|
||||
File.join(embedder.lib_dir, relative_lib_dir),
|
||||
embedder.invocation_dir
|
||||
)
|
||||
end
|
||||
|
||||
def app_bundle_relative_darwin_lib_dir
|
||||
@app_bundle_relative_darwin_lib_dir ||= relative_dir(
|
||||
File.join(embedder.lib_dir, sanitized_relative_darwin_lib_dir),
|
||||
embedder.invocation_dir
|
||||
)
|
||||
end
|
||||
|
||||
@@ -986,6 +1027,10 @@ class GccInfo
|
||||
|
||||
private
|
||||
|
||||
def embedder
|
||||
@embedder ||= AbstractEmbedder.new(Dir.mktmpdir(['Emacs', '.app']))
|
||||
end
|
||||
|
||||
def relative_dir(path, root)
|
||||
Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
|
||||
end
|
||||
@@ -1052,6 +1097,12 @@ if __FILE__ == $PROGRAM_NAME
|
||||
cli_options[:native_full_aot] = v
|
||||
end
|
||||
|
||||
opts.on('--[no-]relink-eln-files',
|
||||
'Enable/disable re-linking shared libraries in bundled *.eln ' \
|
||||
'files (default: enabled)') do |v|
|
||||
cli_options[:relink_eln] = v
|
||||
end
|
||||
|
||||
opts.on('--[no-]rsvg',
|
||||
'Enable/disable SVG image support via librsvg ' \
|
||||
'(default: enabled)') do |v|
|
||||
|
||||
1
go.mod
1
go.mod
@@ -13,6 +13,7 @@ require (
|
||||
github.com/hashicorp/go-hclog v0.16.1
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
|
||||
github.com/hexops/gotextdiff v1.0.3
|
||||
github.com/jimeh/undent v1.1.0
|
||||
github.com/mattn/go-isatty v0.0.13 // indirect
|
||||
github.com/mitchellh/gon v0.2.3
|
||||
|
||||
2
go.sum
2
go.sum
@@ -136,6 +136,8 @@ github.com/hashicorp/go-retryablehttp v0.7.0/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER
|
||||
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
|
||||
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
|
||||
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||
github.com/jimeh/undent v1.1.0 h1:Cge7P4Ws6buy0SVuHBluY/aOKdFuJUMzoJswfAHZ4zE=
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
diff --git a/lisp/emacs-lisp/comp.el b/lisp/emacs-lisp/comp.el
|
||||
index 8c638312b0..87af889ef4 100644
|
||||
index 638d4b274c..2599211936 100644
|
||||
--- a/lisp/emacs-lisp/comp.el
|
||||
+++ b/lisp/emacs-lisp/comp.el
|
||||
@@ -4215,6 +4215,52 @@ native-compile-async
|
||||
@@ -4224,6 +4224,52 @@ native-compile-async
|
||||
(let ((load (not (not load))))
|
||||
(native--compile-async files recursively load selector)))
|
||||
|
||||
@@ -16,10 +16,10 @@ index 8c638312b0..87af889ef4 100644
|
||||
+ (devtools-dir
|
||||
+ "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib")
|
||||
+ (gcc-dir (expand-file-name
|
||||
+ "<%= relative_lib_dir %>"
|
||||
+ "<%= app_bundle_relative_lib_dir %>"
|
||||
+ invocation-directory))
|
||||
+ (darwin-dir (expand-file-name
|
||||
+ "<%= sanitized_relative_darwin_lib_dir %>"
|
||||
+ "<%= app_bundle_relative_darwin_lib_dir %>"
|
||||
+ invocation-directory))
|
||||
+ (lib-paths (list)))
|
||||
+
|
||||
|
||||
13
pkg/cask/live_check.go
Normal file
13
pkg/cask/live_check.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package cask
|
||||
|
||||
type LiveCheck struct {
|
||||
Cask string `json:"cask"`
|
||||
Version LiveCheckVersion `json:"version"`
|
||||
}
|
||||
|
||||
type LiveCheckVersion struct {
|
||||
Current string `json:"current"`
|
||||
Latest string `json:"latest"`
|
||||
Outdated bool `json:"outdated"`
|
||||
NewerThanUpstream bool `json:"newer_than_upstream"`
|
||||
}
|
||||
60
pkg/cask/release_info.go
Normal file
60
pkg/cask/release_info.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package cask
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ReleaseInfo struct {
|
||||
Name string
|
||||
Version string
|
||||
Assets map[string]*ReleaseAsset
|
||||
}
|
||||
|
||||
func (s *ReleaseInfo) Asset(nameMatch string) *ReleaseAsset {
|
||||
if a, ok := s.Assets[nameMatch]; ok {
|
||||
return a
|
||||
}
|
||||
|
||||
// Dirty and inefficient way to ensure assets are searched in a predictable
|
||||
// order.
|
||||
var assets []*ReleaseAsset
|
||||
for _, a := range s.Assets {
|
||||
assets = append(assets, a)
|
||||
}
|
||||
sort.SliceStable(assets, func(i, j int) bool {
|
||||
return assets[i].Filename < assets[j].Filename
|
||||
})
|
||||
|
||||
for _, a := range assets {
|
||||
if strings.Contains(a.Filename, nameMatch) {
|
||||
return a
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ReleaseInfo) DownloadURL(nameMatch string) string {
|
||||
a := s.Asset(nameMatch)
|
||||
if a == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.DownloadURL
|
||||
}
|
||||
|
||||
func (s *ReleaseInfo) SHA256(nameMatch string) string {
|
||||
a := s.Asset(nameMatch)
|
||||
if a == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return a.SHA256
|
||||
}
|
||||
|
||||
type ReleaseAsset struct {
|
||||
Filename string
|
||||
DownloadURL string
|
||||
SHA256 string
|
||||
}
|
||||
493
pkg/cask/update.go
Normal file
493
pkg/cask/update.go
Normal file
@@ -0,0 +1,493 @@
|
||||
package cask
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-github/v35/github"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hexops/gotextdiff"
|
||||
"github.com/hexops/gotextdiff/myers"
|
||||
"github.com/hexops/gotextdiff/span"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/release"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
|
||||
)
|
||||
|
||||
// Error vars
|
||||
var (
|
||||
Err = errors.New("cask")
|
||||
ErrReleaseNotFound = fmt.Errorf("%w: release not found", Err)
|
||||
|
||||
ErrFailedSHA256Parse = fmt.Errorf(
|
||||
"%w: failed to parse SHA256 from asset", Err,
|
||||
)
|
||||
ErrFailedSHA256Download = fmt.Errorf(
|
||||
"%w: failed to download SHA256 asset", Err,
|
||||
)
|
||||
ErrNoTapOrOutput = fmt.Errorf(
|
||||
"%w: no tap repository or output directory specified", Err,
|
||||
)
|
||||
)
|
||||
|
||||
type UpdateOptions struct {
|
||||
// BuildsRepo is the GitHub repository containing binary releases.
|
||||
BuildsRepo *repository.Repository
|
||||
|
||||
// TapRepo is the GitHub repository to update the casks in.
|
||||
TapRepo *repository.Repository
|
||||
|
||||
// Ref is the git ref to apply cask updates on top of. Default branch will
|
||||
// be used if empty.
|
||||
Ref string
|
||||
|
||||
// OutputDir specifies a directory to write cask files to. When set, tap
|
||||
// repository is ignored and no changes will be committed directly against
|
||||
// any specified tap repository.
|
||||
OutputDir string
|
||||
|
||||
// Force update will ignore the outdated live check flag, and process all
|
||||
// casks regardless. But it will only update the cask in question if the
|
||||
// resulting output cask is different.
|
||||
Force bool
|
||||
|
||||
// TemplatesDir is the directory where cask templates are located.
|
||||
TemplatesDir string
|
||||
|
||||
LiveChecks []*LiveCheck
|
||||
|
||||
GithubToken string
|
||||
}
|
||||
|
||||
type Updater struct {
|
||||
BuildsRepo *repository.Repository
|
||||
TapRepo *repository.Repository
|
||||
Ref string
|
||||
OutputDir string
|
||||
TemplatesDir string
|
||||
|
||||
logger hclog.Logger
|
||||
gh *github.Client
|
||||
}
|
||||
|
||||
func Update(ctx context.Context, opts *UpdateOptions) error {
|
||||
updater := &Updater{
|
||||
BuildsRepo: opts.BuildsRepo,
|
||||
TapRepo: opts.TapRepo,
|
||||
Ref: opts.Ref,
|
||||
OutputDir: opts.OutputDir,
|
||||
TemplatesDir: opts.TemplatesDir,
|
||||
logger: hclog.FromContext(ctx).Named("cask"),
|
||||
gh: gh.New(ctx, opts.GithubToken),
|
||||
}
|
||||
|
||||
for _, chk := range opts.LiveChecks {
|
||||
err := updater.Update(ctx, chk, opts.Force)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Updater) Update(
|
||||
ctx context.Context,
|
||||
chk *LiveCheck,
|
||||
force bool,
|
||||
) error {
|
||||
if s.TapRepo == nil && s.OutputDir == "" {
|
||||
return ErrNoTapOrOutput
|
||||
}
|
||||
|
||||
if !force && !chk.Version.Outdated {
|
||||
s.logger.Info("skipping", "cask", chk.Cask, "reason", "up to date")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
newCaskContent, err := s.renderCask(ctx, chk)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
caskFile := chk.Cask + ".rb"
|
||||
|
||||
if s.OutputDir != "" {
|
||||
_, err = s.putFile(
|
||||
ctx, chk, filepath.Join(s.OutputDir, caskFile), newCaskContent,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = s.putRepoFile(
|
||||
ctx, s.TapRepo, s.Ref, chk,
|
||||
filepath.Join("Casks", caskFile), newCaskContent,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Updater) putFile(
|
||||
ctx context.Context,
|
||||
chk *LiveCheck,
|
||||
filename string,
|
||||
content []byte,
|
||||
) (bool, error) {
|
||||
parent := filepath.Dir(filename)
|
||||
s.logger.Info("processing cask update",
|
||||
"output-directory", parent, "cask", chk.Cask, "file", filename,
|
||||
)
|
||||
|
||||
err := os.MkdirAll(parent, 0o755)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
existingContent, err := os.ReadFile(filename)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return false, err
|
||||
}
|
||||
|
||||
infoMsg := "creating cask"
|
||||
|
||||
if !os.IsNotExist(err) {
|
||||
infoMsg = "updating cask"
|
||||
if bytes.Equal(existingContent, content) {
|
||||
s.logger.Info(
|
||||
"skip update: no change to cask content",
|
||||
"cask", chk.Cask, "file", filename,
|
||||
)
|
||||
|
||||
s.logger.Debug(
|
||||
"cask content",
|
||||
"file", filename, "content", string(content),
|
||||
)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
existing := string(existingContent)
|
||||
edits := myers.ComputeEdits(
|
||||
span.URIFromPath(filename), existing, string(content),
|
||||
)
|
||||
diff := fmt.Sprint(gotextdiff.ToUnified(
|
||||
filename, filename, existing, edits,
|
||||
))
|
||||
|
||||
s.logger.Info(
|
||||
infoMsg,
|
||||
"cask", chk.Cask, "version", chk.Version.Latest, "file", filename,
|
||||
"diff", diff,
|
||||
)
|
||||
|
||||
s.logger.Debug(
|
||||
"cask content",
|
||||
"file", filename, "content", string(content),
|
||||
)
|
||||
|
||||
err = os.WriteFile(filename, content, 0o644) //nolint:gosec
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Updater) putRepoFile(
|
||||
ctx context.Context,
|
||||
repo *repository.Repository,
|
||||
ref string,
|
||||
chk *LiveCheck,
|
||||
filename string,
|
||||
content []byte,
|
||||
) (bool, error) {
|
||||
s.logger.Info("processing cask update",
|
||||
"tap-repo", repo.Source, "cask", chk.Cask, "file", filename,
|
||||
)
|
||||
repoContent, _, resp, err := s.gh.Repositories.GetContents(
|
||||
ctx, repo.Owner(), repo.Name(), filename,
|
||||
&github.RepositoryContentGetOptions{Ref: ref},
|
||||
)
|
||||
if err != nil && resp.StatusCode != http.StatusNotFound {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == http.StatusNotFound {
|
||||
err := s.createRepoFile(ctx, repo, chk, filename, content)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
} else {
|
||||
_, err := s.updateRepoFile(
|
||||
ctx, repo, repoContent, chk, filename, content,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Updater) createRepoFile(
|
||||
ctx context.Context,
|
||||
repo *repository.Repository,
|
||||
chk *LiveCheck,
|
||||
filename string,
|
||||
content []byte,
|
||||
) error {
|
||||
commitMsg := fmt.Sprintf(
|
||||
"feat(cask): create %s with version %s",
|
||||
chk.Cask, chk.Version.Latest,
|
||||
)
|
||||
|
||||
edits := myers.ComputeEdits(
|
||||
span.URIFromPath(filename), "", string(content),
|
||||
)
|
||||
diff := fmt.Sprint(gotextdiff.ToUnified(filename, filename, "", edits))
|
||||
|
||||
s.logger.Info(
|
||||
"creating cask",
|
||||
"cask", chk.Cask, "version", chk.Version.Latest, "file", filename,
|
||||
"diff", diff,
|
||||
)
|
||||
s.logger.Debug(
|
||||
"cask content",
|
||||
"file", filename, "content", string(content),
|
||||
)
|
||||
contResp, _, err := s.gh.Repositories.CreateFile(
|
||||
ctx, repo.Owner(), repo.Name(), filename,
|
||||
&github.RepositoryContentFileOptions{
|
||||
Message: &commitMsg,
|
||||
Content: content,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info(
|
||||
"new commit created",
|
||||
"commit", contResp.GetSHA(), "message", contResp.GetMessage(),
|
||||
"url", contResp.Commit.GetHTMLURL(),
|
||||
)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Updater) updateRepoFile(
|
||||
ctx context.Context,
|
||||
repo *repository.Repository,
|
||||
repoContent *github.RepositoryContent,
|
||||
chk *LiveCheck,
|
||||
filename string,
|
||||
content []byte,
|
||||
) (bool, error) {
|
||||
existingContent, err := repoContent.GetContent()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if existingContent == string(content) {
|
||||
s.logger.Info(
|
||||
"skip update: no change to cask content",
|
||||
"cask", chk.Cask, "file", filename,
|
||||
)
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
sha := repoContent.GetSHA()
|
||||
|
||||
commitMsg := fmt.Sprintf(
|
||||
"feat(cask): update %s to version %s",
|
||||
chk.Cask, chk.Version.Latest,
|
||||
)
|
||||
|
||||
edits := myers.ComputeEdits(
|
||||
span.URIFromPath(filename), existingContent, string(content),
|
||||
)
|
||||
diff := fmt.Sprint(gotextdiff.ToUnified(
|
||||
filename, filename, existingContent, edits,
|
||||
))
|
||||
|
||||
s.logger.Info(
|
||||
"updating cask",
|
||||
"cask", chk.Cask, "version", chk.Version.Latest, "file", filename,
|
||||
"diff", diff,
|
||||
)
|
||||
s.logger.Debug(
|
||||
"cask content",
|
||||
"file", filename, "content", string(content),
|
||||
)
|
||||
|
||||
contResp, _, err := s.gh.Repositories.CreateFile(
|
||||
ctx, repo.Owner(), repo.Name(), filename,
|
||||
&github.RepositoryContentFileOptions{
|
||||
Message: &commitMsg,
|
||||
Content: content,
|
||||
SHA: &sha,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
s.logger.Info(
|
||||
"new commit created",
|
||||
"commit", contResp.GetSHA(), "message", contResp.GetMessage(),
|
||||
"url", contResp.Commit.GetHTMLURL(),
|
||||
)
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *Updater) renderCask(
|
||||
ctx context.Context,
|
||||
chk *LiveCheck,
|
||||
) ([]byte, error) {
|
||||
releaseName, err := release.VersionToName(chk.Version.Latest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("fetching release details",
|
||||
"release", releaseName, "repo", s.BuildsRepo.URL(),
|
||||
)
|
||||
release, resp, err := s.gh.Repositories.GetReleaseByTag(
|
||||
ctx, s.BuildsRepo.Owner(), s.BuildsRepo.Name(), releaseName,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if release == nil || resp.StatusCode == http.StatusNotFound {
|
||||
return nil, fmt.Errorf("%w: %s", ErrReleaseNotFound, releaseName)
|
||||
}
|
||||
|
||||
info := &ReleaseInfo{
|
||||
Name: release.GetName(),
|
||||
Version: chk.Version.Latest,
|
||||
Assets: map[string]*ReleaseAsset{},
|
||||
}
|
||||
|
||||
s.logger.Info("processing release assets")
|
||||
for _, asset := range release.Assets {
|
||||
filename := asset.GetName()
|
||||
s.logger.Debug("processing asset", "filename", filename)
|
||||
|
||||
if strings.HasSuffix(filename, ".sha256") {
|
||||
filename = strings.TrimSuffix(filename, ".sha256")
|
||||
}
|
||||
|
||||
if _, ok := info.Assets[filename]; !ok {
|
||||
info.Assets[filename] = &ReleaseAsset{
|
||||
Filename: filename,
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasSuffix(asset.GetName(), ".sha256") {
|
||||
s.logger.Debug("downloading *.sha256 asset to extract SHA256 value")
|
||||
r, err2 := s.downloadAssetContent(ctx, asset)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
defer r.Close()
|
||||
|
||||
content := make([]byte, 64)
|
||||
n, err2 := io.ReadAtLeast(r, content, 64)
|
||||
if err2 != nil {
|
||||
return nil, err2
|
||||
}
|
||||
if n < 64 {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: %s", ErrFailedSHA256Parse, asset.GetName(),
|
||||
)
|
||||
}
|
||||
|
||||
sha := string(content)[0:64]
|
||||
if sha == "" {
|
||||
return nil, fmt.Errorf(
|
||||
"%w: %s", ErrFailedSHA256Parse, asset.GetName(),
|
||||
)
|
||||
}
|
||||
|
||||
info.Assets[filename].SHA256 = sha
|
||||
} else {
|
||||
info.Assets[filename].DownloadURL = asset.GetBrowserDownloadURL()
|
||||
}
|
||||
}
|
||||
|
||||
templateFile := filepath.Join(s.TemplatesDir, chk.Cask+".rb.tpl")
|
||||
tplContent, err := os.ReadFile(templateFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tpl, err := template.New(chk.Cask).Parse(string(tplContent))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = tpl.Execute(&buf, info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (s *Updater) downloadAssetContent(
|
||||
ctx context.Context,
|
||||
asset *github.ReleaseAsset,
|
||||
) (io.ReadCloser, error) {
|
||||
httpClient := &http.Client{Timeout: 60 * time.Second}
|
||||
|
||||
r, downloadURL, err := s.gh.Repositories.DownloadReleaseAsset(
|
||||
ctx, s.BuildsRepo.Owner(), s.BuildsRepo.Name(),
|
||||
asset.GetID(), httpClient,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if r == nil && downloadURL != "" {
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", downloadURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
//nolint:bodyclose
|
||||
resp, err := httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r = resp.Body
|
||||
}
|
||||
|
||||
if r == nil {
|
||||
return nil, fmt.Errorf(
|
||||
"%s: %s", ErrFailedSHA256Download, asset.GetName(),
|
||||
)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
}
|
||||
158
pkg/cli/cask.go
Normal file
158
pkg/cli/cask.go
Normal file
@@ -0,0 +1,158 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/cask"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
|
||||
cli2 "github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
type caskOptions struct {
|
||||
BuildsRepo *repository.Repository
|
||||
GithubToken string
|
||||
}
|
||||
|
||||
func caskCmd() *cli2.Command {
|
||||
tokenDefaultText := ""
|
||||
if len(os.Getenv("GITHUB_TOKEN")) > 0 {
|
||||
tokenDefaultText = "***"
|
||||
}
|
||||
|
||||
return &cli2.Command{
|
||||
Name: "cask",
|
||||
Usage: "manage Homebrew Casks",
|
||||
Flags: []cli2.Flag{
|
||||
&cli2.StringFlag{
|
||||
Name: "builds-repository",
|
||||
Aliases: []string{"builds-repo", "b"},
|
||||
Usage: "owner/name of GitHub repo for containing builds",
|
||||
EnvVars: []string{"EMACS_BUILDS_REPOSITORY"},
|
||||
Value: "jimeh/emacs-builds",
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "github-token",
|
||||
Usage: "GitHub API Token",
|
||||
EnvVars: []string{"GITHUB_TOKEN"},
|
||||
DefaultText: tokenDefaultText,
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Subcommands: []*cli2.Command{
|
||||
caskUpdateCmd(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func caskActionWrapper(
|
||||
f func(*cli2.Context, *Options, *caskOptions) error,
|
||||
) func(*cli2.Context) error {
|
||||
return actionWrapper(func(c *cli2.Context, opts *Options) error {
|
||||
rOpts := &caskOptions{
|
||||
GithubToken: c.String("github-token"),
|
||||
}
|
||||
|
||||
if r := c.String("builds-repository"); r != "" {
|
||||
var err error
|
||||
rOpts.BuildsRepo, err = repository.NewGitHub(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return f(c, opts, rOpts)
|
||||
})
|
||||
}
|
||||
|
||||
func caskUpdateCmd() *cli2.Command {
|
||||
return &cli2.Command{
|
||||
Name: "update",
|
||||
Usage: "update casks based on brew livecheck result in JSON format",
|
||||
ArgsUsage: "<livecheck.json>",
|
||||
Flags: []cli2.Flag{
|
||||
&cli2.StringFlag{
|
||||
Name: "ref",
|
||||
Usage: "git ref to create/update casks on top of in the " +
|
||||
"tap repository",
|
||||
EnvVars: []string{"GITHUB_REF"},
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "directory to write cask files to",
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "tap-repository",
|
||||
Aliases: []string{"tap"},
|
||||
Usage: "owner/name of GitHub repo for Homebrew Tap to " +
|
||||
"commit changes to if --output is not set",
|
||||
EnvVars: []string{"GITHUB_REPOSITORY"},
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "templates-dir",
|
||||
Aliases: []string{"t"},
|
||||
Usage: "path to directory of cask templates",
|
||||
EnvVars: []string{"CASK_TEMPLATE_DIR"},
|
||||
Required: true,
|
||||
},
|
||||
&cli2.BoolFlag{
|
||||
Name: "force",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "force update file even if livecheck has it marked " +
|
||||
"as not outdated (does not force update if cask " +
|
||||
"content is unchanged)",
|
||||
Value: false,
|
||||
},
|
||||
},
|
||||
Action: caskActionWrapper(caskUpdateAction),
|
||||
}
|
||||
}
|
||||
|
||||
func caskUpdateAction(
|
||||
c *cli2.Context,
|
||||
opts *Options,
|
||||
cOpts *caskOptions,
|
||||
) error {
|
||||
updateOpts := &cask.UpdateOptions{
|
||||
BuildsRepo: cOpts.BuildsRepo,
|
||||
GithubToken: cOpts.GithubToken,
|
||||
Ref: c.String("ref"),
|
||||
OutputDir: c.String("output"),
|
||||
Force: c.Bool("force"),
|
||||
TemplatesDir: c.String("templates-dir"),
|
||||
}
|
||||
|
||||
if r := c.String("tap-repository"); r != "" {
|
||||
var err error
|
||||
updateOpts.TapRepo, err = repository.NewGitHub(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
arg := c.Args().First()
|
||||
if arg == "" {
|
||||
return errors.New("no livecheck argument given")
|
||||
}
|
||||
|
||||
if arg == "-" {
|
||||
err := json.NewDecoder(c.App.Reader).Decode(&updateOpts.LiveChecks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
f, err := os.Open(arg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = json.NewDecoder(f).Decode(&updateOpts.LiveChecks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return cask.Update(c.Context, updateOpts)
|
||||
}
|
||||
@@ -49,6 +49,7 @@ func New(version, commit, date string) *CLI {
|
||||
notarizeCmd(),
|
||||
packageCmd(),
|
||||
releaseCmd(),
|
||||
caskCmd(),
|
||||
{
|
||||
Name: "version",
|
||||
Usage: "print the version",
|
||||
|
||||
@@ -89,7 +89,7 @@ func planAction(c *cli2.Context, opts *Options) error {
|
||||
GithubToken: c.String("github-token"),
|
||||
}
|
||||
|
||||
if c.String("test-build-type") == "draft" {
|
||||
if c.String("test-release-type") == "draft" {
|
||||
planOpts.TestBuildType = plan.Draft
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -60,6 +61,7 @@ func releaseCmd() *cli2.Command {
|
||||
Subcommands: []*cli2.Command{
|
||||
releaseCheckCmd(),
|
||||
releasePublishCmd(),
|
||||
releaseBulkCmd(),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -206,3 +208,56 @@ func releasePublishAction(
|
||||
|
||||
return release.Publish(c.Context, rlsOpts)
|
||||
}
|
||||
|
||||
func releaseBulkCmd() *cli2.Command {
|
||||
return &cli2.Command{
|
||||
Name: "bulk",
|
||||
Usage: "bulk modify GitHub releases",
|
||||
ArgsUsage: "",
|
||||
Flags: []cli2.Flag{
|
||||
&cli2.StringFlag{
|
||||
Name: "name",
|
||||
Usage: "regexp pattern matching release names to modify",
|
||||
},
|
||||
&cli2.StringFlag{
|
||||
Name: "prerelease",
|
||||
Usage: "change prerelease flag, must be \"true\" or " +
|
||||
"\"false\", otherwise prerelease value is not changed",
|
||||
},
|
||||
&cli2.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "do not perform any changes",
|
||||
},
|
||||
},
|
||||
Action: releaseActionWrapper(releaseBulkAction),
|
||||
}
|
||||
}
|
||||
|
||||
func releaseBulkAction(
|
||||
c *cli2.Context,
|
||||
opts *Options,
|
||||
rOpts *releaseOptions,
|
||||
) error {
|
||||
bulkOpts := &release.BulkOptions{
|
||||
Repository: rOpts.Repository,
|
||||
NamePattern: c.String("name"),
|
||||
DryRun: c.Bool("dry-run"),
|
||||
GithubToken: rOpts.GithubToken,
|
||||
}
|
||||
|
||||
switch c.String("prerelease") {
|
||||
case "true":
|
||||
v := true
|
||||
bulkOpts.Prerelease = &v
|
||||
case "false":
|
||||
v := false
|
||||
bulkOpts.Prerelease = &v
|
||||
case "":
|
||||
default:
|
||||
return errors.New(
|
||||
"--prerelease by me \"true\" or \"false\" when specified",
|
||||
)
|
||||
}
|
||||
|
||||
return release.Bulk(c.Context, bulkOpts)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/commit"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/osinfo"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/release"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
|
||||
)
|
||||
|
||||
@@ -64,15 +65,17 @@ func Create(ctx context.Context, opts *Options) (*Plan, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
releaseName := fmt.Sprintf(
|
||||
"Emacs.%s.%s.%s",
|
||||
version := fmt.Sprintf(
|
||||
"%s.%s.%s",
|
||||
commitInfo.DateString(),
|
||||
commitInfo.ShortSHA(),
|
||||
sanitizeString(opts.Ref),
|
||||
)
|
||||
|
||||
releaseName := fmt.Sprintf("Emacs.%s", version)
|
||||
buildName := fmt.Sprintf(
|
||||
"%s.%s.%s",
|
||||
releaseName,
|
||||
"Emacs.%s.%s.%s",
|
||||
version,
|
||||
sanitizeString(osInfo.Name+"-"+osInfo.MajorMinor()),
|
||||
sanitizeString(osInfo.Arch),
|
||||
)
|
||||
@@ -92,7 +95,8 @@ func Create(ctx context.Context, opts *Options) (*Plan, error) {
|
||||
},
|
||||
OS: osInfo,
|
||||
Release: &Release{
|
||||
Name: releaseName,
|
||||
Name: releaseName,
|
||||
Prerelease: true,
|
||||
},
|
||||
Output: &Output{
|
||||
Directory: opts.OutputDir,
|
||||
@@ -100,16 +104,28 @@ func Create(ctx context.Context, opts *Options) (*Plan, error) {
|
||||
},
|
||||
}
|
||||
|
||||
// If given git ref is a stable release tag (emacs-23.2b, emacs-27.2, etc.)
|
||||
// we modify release properties accordingly.
|
||||
if v, err := release.GitRefToStableVersion(opts.Ref); err == nil {
|
||||
plan.Release.Prerelease = false
|
||||
plan.Release.Name, err = release.VersionToName(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
if opts.TestBuild != "" {
|
||||
testName := sanitizeString(opts.TestBuild)
|
||||
|
||||
plan.Build.Name += ".test." + testName
|
||||
plan.Release.Title = "Test Builds"
|
||||
plan.Release.Name = "test-builds"
|
||||
|
||||
plan.Release.Prerelease = true
|
||||
plan.Release.Draft = false
|
||||
if opts.TestBuildType == Draft {
|
||||
plan.Release.Prerelease = false
|
||||
plan.Release.Draft = true
|
||||
} else {
|
||||
plan.Release.Prerelease = true
|
||||
}
|
||||
|
||||
index := strings.LastIndex(diskImage, ".")
|
||||
|
||||
84
pkg/release/bulk.go
Normal file
84
pkg/release/bulk.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package release
|
||||
|
||||
import (
|
||||
"context"
|
||||
"regexp"
|
||||
|
||||
"github.com/google/go-github/v35/github"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/gh"
|
||||
"github.com/jimeh/build-emacs-for-macos/pkg/repository"
|
||||
)
|
||||
|
||||
type BulkOptions struct {
|
||||
Repository *repository.Repository
|
||||
NamePattern string
|
||||
Prerelease *bool
|
||||
DryRun bool
|
||||
GithubToken string
|
||||
}
|
||||
|
||||
func Bulk(ctx context.Context, opts *BulkOptions) error {
|
||||
logger := hclog.FromContext(ctx).Named("release")
|
||||
gh := gh.New(ctx, opts.GithubToken)
|
||||
|
||||
nameMatcher, err := regexp.Compile(opts.NamePattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nextPage := 1
|
||||
lastPage := 1
|
||||
|
||||
for nextPage <= lastPage {
|
||||
releases, resp, err := gh.Repositories.ListReleases(
|
||||
ctx, opts.Repository.Owner(), opts.Repository.Name(),
|
||||
&github.ListOptions{
|
||||
Page: nextPage,
|
||||
PerPage: 100,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nextPage = resp.NextPage
|
||||
lastPage = resp.LastPage
|
||||
|
||||
for _, r := range releases {
|
||||
if !nameMatcher.MatchString(r.GetName()) {
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Info("match found", "release", r.GetName())
|
||||
|
||||
var changes []interface{}
|
||||
if opts.Prerelease != nil && r.GetPrerelease() != *opts.Prerelease {
|
||||
changes = append(changes, "prerelease", *opts.Prerelease)
|
||||
r.Prerelease = opts.Prerelease
|
||||
}
|
||||
|
||||
if len(changes) > 0 {
|
||||
changes = append(
|
||||
[]interface{}{"release", r.GetName()}, changes...,
|
||||
)
|
||||
logger.Info("modifying", changes...)
|
||||
if !opts.DryRun {
|
||||
_, _, err = gh.Repositories.EditRelease(
|
||||
ctx, opts.Repository.Owner(), opts.Repository.Name(),
|
||||
r.GetID(), r,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if nextPage == 0 || lastPage == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
42
pkg/release/version.go
Normal file
42
pkg/release/version.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package release
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
// Errors
|
||||
var (
|
||||
Err = errors.New("release")
|
||||
ErrInvalidName = fmt.Errorf("%w: invalid name", Err)
|
||||
ErrEmptyVersion = fmt.Errorf("%w: empty version", Err)
|
||||
ErrNotStableRef = fmt.Errorf(
|
||||
"%w: git ref is not stable tagged release", Err,
|
||||
)
|
||||
)
|
||||
|
||||
var (
|
||||
stableVersion = regexp.MustCompile(`^\d+\.\d+(?:[a-z]+)?$`)
|
||||
stableGetRef = regexp.MustCompile(`^emacs-(\d+\.\d+(?:[a-z]+)?)$`)
|
||||
)
|
||||
|
||||
func VersionToName(version string) (string, error) {
|
||||
if version == "" {
|
||||
return "", ErrEmptyVersion
|
||||
}
|
||||
|
||||
if stableVersion.MatchString(version) {
|
||||
return "Emacs-" + version, nil
|
||||
}
|
||||
|
||||
return "Emacs." + version, nil
|
||||
}
|
||||
|
||||
func GitRefToStableVersion(ref string) (string, error) {
|
||||
if m := stableGetRef.FindStringSubmatch(ref); len(m) > 1 {
|
||||
return m[1], nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("%w: \"%s\"", ErrNotStableRef, ref)
|
||||
}
|
||||
138
pkg/release/version_test.go
Normal file
138
pkg/release/version_test.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package release
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestVersionToName(t *testing.T) {
|
||||
type args struct {
|
||||
version string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
version: "",
|
||||
},
|
||||
wantErr: "release: empty version",
|
||||
},
|
||||
{
|
||||
name: "nightly",
|
||||
args: args{
|
||||
version: "2021-07-01.1b88404.master",
|
||||
},
|
||||
want: "Emacs.2021-07-01.1b88404.master",
|
||||
},
|
||||
{
|
||||
name: "stable",
|
||||
args: args{
|
||||
version: "27.2",
|
||||
},
|
||||
want: "Emacs-27.2",
|
||||
},
|
||||
{
|
||||
name: "stable with letter",
|
||||
args: args{
|
||||
version: "23.3b",
|
||||
},
|
||||
want: "Emacs-23.3b",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := VersionToName(tt.args.version)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGitRefToStableVersion(t *testing.T) {
|
||||
type args struct {
|
||||
version string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr string
|
||||
}{
|
||||
{
|
||||
name: "empty",
|
||||
args: args{
|
||||
version: "",
|
||||
},
|
||||
wantErr: "release: git ref is not stable tagged release: \"\"",
|
||||
},
|
||||
{
|
||||
name: "master",
|
||||
args: args{
|
||||
version: "master",
|
||||
},
|
||||
wantErr: "release: git ref is not stable tagged release: " +
|
||||
"\"master\"",
|
||||
},
|
||||
{
|
||||
name: "feature",
|
||||
args: args{
|
||||
version: "feature/native-comp",
|
||||
},
|
||||
wantErr: "release: git ref is not stable tagged release: " +
|
||||
"\"feature/native-comp\"",
|
||||
},
|
||||
{
|
||||
name: "stable",
|
||||
args: args{
|
||||
version: "emacs-27.2",
|
||||
},
|
||||
want: "27.2",
|
||||
},
|
||||
{
|
||||
name: "stable with letter",
|
||||
args: args{
|
||||
version: "emacs-23.3b",
|
||||
},
|
||||
want: "23.3b",
|
||||
},
|
||||
{
|
||||
name: "future stable",
|
||||
args: args{
|
||||
version: "emacs-239.33",
|
||||
},
|
||||
want: "239.33",
|
||||
},
|
||||
{
|
||||
name: "future stable with letter",
|
||||
args: args{
|
||||
version: "emacs-239.33c",
|
||||
},
|
||||
want: "239.33c",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := GitRefToStableVersion(tt.args.version)
|
||||
|
||||
assert.Equal(t, tt.want, got)
|
||||
|
||||
if tt.wantErr != "" {
|
||||
assert.EqualError(t, err, tt.wantErr)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -117,42 +117,24 @@ func signCLIHelper(ctx context.Context, appBundle string, opts *Options) error {
|
||||
}
|
||||
|
||||
// elnFiles finds all native-compilation *.eln files within a Emacs.app bundle,
|
||||
// based on expected paths they might be stored in.
|
||||
// excluding any *.eln which should be automatically located by codesign when
|
||||
// signing the Emacs.app bundle itself with the --deep flag. Essentially this
|
||||
// only returns *.eln files which must be individually signed before signing the
|
||||
// app bundle itself.
|
||||
func elnFiles(emacsApp string) ([]string, error) {
|
||||
dirs := []string{
|
||||
// Current *.eln location.
|
||||
filepath.Join(emacsApp, "Contents", "Resources", "native-lisp"),
|
||||
// Legacy *.eln location.
|
||||
filepath.Join(emacsApp, "Contents", "MacOS", "lib", "emacs"),
|
||||
}
|
||||
|
||||
var files []string
|
||||
walkDirFunc := func(path string, _d fs.DirEntry, _err error) error {
|
||||
if strings.HasSuffix(path, ".eln") {
|
||||
walkDirFunc := func(path string, d fs.DirEntry, _err error) error {
|
||||
if d.Type().IsRegular() && strings.HasSuffix(path, ".eln") &&
|
||||
!strings.Contains(path, ".app/Contents/Frameworks/") {
|
||||
files = append(files, path)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, dir := range dirs {
|
||||
fi, err := os.Stat(dir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !fi.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
err = filepath.WalkDir(dir, walkDirFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err := filepath.WalkDir(filepath.Join(emacsApp, "Contents"), walkDirFunc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return files, nil
|
||||
|
||||
Reference in New Issue
Block a user