dev/release/binary-task.rb (2,283 lines of code) (raw):
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
require "cgi/util"
require "digest/sha1"
require "digest/sha2"
require "io/console"
require "json"
require "net/http"
require "pathname"
require "tempfile"
require "thread"
require "time"
begin
require "apt-dists-merge"
rescue LoadError
warn("apt-dists-merge is needed for apt:* tasks")
end
class BinaryTask
include Rake::DSL
class ThreadPool
def initialize(use_case, &worker)
@n_workers = choose_n_workers(use_case)
@worker = worker
@jobs = Thread::Queue.new
@workers = @n_workers.times.collect do
Thread.new do
loop do
job = @jobs.pop
break if job.nil?
@worker.call(job)
end
end
end
end
def <<(job)
@jobs << job
end
def join
@n_workers.times do
@jobs << nil
end
@workers.each(&:join)
end
private
def choose_n_workers(use_case)
case use_case
when :artifactory
# Too many workers cause Artifactory error.
6
when :maven_repository
# Too many workers break ASF policy:
# https://infra.apache.org/infra-ban.html
4
when :gpg
# Too many workers cause gpg-agent error.
2
else
raise "Unknown use case: #{use_case}"
end
end
end
class ProgressReporter
def initialize(label, count_max=0)
@label = label
@count_max = count_max
@mutex = Thread::Mutex.new
@time_start = Time.now
@time_previous = Time.now
@count_current = 0
@count_previous = 0
end
def advance
@mutex.synchronize do
@count_current += 1
return if @count_max.zero?
time_current = Time.now
if time_current - @time_previous <= 1
return
end
show_progress(time_current)
end
end
def increment_max
@mutex.synchronize do
@count_max += 1
show_progress(Time.now) if @count_max == 1
end
end
def finish
@mutex.synchronize do
return if @count_max.zero?
show_progress(Time.now)
$stderr.puts
end
end
private
def show_progress(time_current)
n_finishes = @count_current - @count_previous
throughput = n_finishes.to_f / (time_current - @time_previous)
@time_previous = time_current
@count_previous = @count_current
message = build_message(time_current, throughput)
$stderr.print("\r#{message}") if message
end
def build_message(time_current, throughput)
percent = (@count_current / @count_max.to_f) * 100
formatted_count = "[%s/%s]" % [
format_count(@count_current),
format_count(@count_max),
]
elapsed_second = time_current - @time_start
if throughput.zero?
rest_second = 0
else
rest_second = (@count_max - @count_current) / throughput
end
separator = " - "
progress = "%5.1f%% %s %s %s %s" % [
percent,
formatted_count,
format_time_interval(elapsed_second),
format_time_interval(rest_second),
format_throughput(throughput),
]
label = @label
width = guess_terminal_width
return "#{label}#{separator}#{progress}" if width.nil?
return nil if progress.size > width
label_width = width - progress.size - separator.size
if label.size > label_width
ellipsis = "..."
shorten_label_width = label_width - ellipsis.size
if shorten_label_width < 1
return progress
else
label = label[0, shorten_label_width] + ellipsis
end
end
"#{label}#{separator}#{progress}"
end
def format_count(count)
"%d" % count
end
def format_time_interval(interval)
if interval < 60
"00:00:%02d" % interval
elsif interval < (60 * 60)
minute, second = interval.divmod(60)
"00:%02d:%02d" % [minute, second]
elsif interval < (60 * 60 * 24)
minute, second = interval.divmod(60)
hour, minute = minute.divmod(60)
"%02d:%02d:%02d" % [hour, minute, second]
else
minute, second = interval.divmod(60)
hour, minute = minute.divmod(60)
day, hour = hour.divmod(24)
"%dd %02d:%02d:%02d" % [day, hour, minute, second]
end
end
def format_throughput(throughput)
"%2d/s" % throughput
end
def guess_terminal_width
guess_terminal_width_from_io ||
guess_terminal_width_from_command ||
guess_terminal_width_from_env ||
80
end
def guess_terminal_width_from_io
if IO.respond_to?(:console) and IO.console
IO.console.winsize[1]
elsif $stderr.respond_to?(:winsize)
begin
$stderr.winsize[1]
rescue SystemCallError
nil
end
else
nil
end
end
def guess_terminal_width_from_command
IO.pipe do |input, output|
begin
pid = spawn("tput", "cols", {:out => output, :err => output})
rescue SystemCallError
return nil
end
output.close
_, status = Process.waitpid2(pid)
return nil unless status.success?
result = input.read.chomp
begin
Integer(result, 10)
rescue ArgumentError
nil
end
end
end
def guess_terminal_width_from_env
env = ENV["COLUMNS"] || ENV["TERM_WIDTH"]
return nil if env.nil?
begin
Integer(env, 10)
rescue ArgumentError
nil
end
end
end
class HTTPClient
class Error < StandardError
attr_reader :request
attr_reader :response
def initialize(request, response, message)
@request = request
@response = response
super(message)
end
end
def initialize
@http = nil
@current_timeout = nil
end
private def start_http(url, &block)
http = Net::HTTP.new(url.host, url.port)
http.set_debug_output($stderr) if ENV["DEBUG"]
http.use_ssl = true
if block_given?
http.start(&block)
else
http
end
end
def close
return if @http.nil?
@http.finish if @http.started?
@http = nil
end
def request(method, headers, url, body: nil, &block)
request = build_request(method, url, headers, body: body)
if ENV["DRY_RUN"] == "yes"
case request
when Net::HTTP::Get, Net::HTTP::Head
else
p [method, url]
return
end
end
@http ||= start_http(url)
request_internal(@http, request, &block)
end
private def request_internal(http, request, &block)
read_timeout = http.read_timeout
begin
http.read_timeout = @current_timeout if @current_timeout
http.request(request) do |response|
case response
when Net::HTTPSuccess,
Net::HTTPNotModified
if block_given?
return yield(response)
else
response.read_body
return response
end
when Net::HTTPRedirection
redirected_url = URI(response["Location"])
redirected_request = Net::HTTP::Get.new(redirected_url, {})
start_http(redirected_url) do |redirected_http|
request_internal(redirected_http, redirected_request, &block)
end
else
message = "failed to request: "
message << "#{request.uri}: #{request.method}: "
message << "#{response.message} #{response.code}"
if response.body
message << "\n"
message << response.body
end
raise Error.new(request, response, message)
end
end
ensure
http.read_timeout = read_timeout
end
end
def head(path)
url = build_read_url(path)
with_retry(3, url) do
request(:head, {}, url)
end
end
def exist?(path)
begin
head(path)
true
rescue Error => error
case error.response
when Net::HTTPNotFound
false
else
raise
end
end
end
def download(path, output_path=nil)
url = build_read_url(path)
with_retry(5, url) do
begin
begin
headers = {}
if output_path and File.exist?(output_path)
headers["If-Modified-Since"] = File.mtime(output_path).rfc2822
end
request(:get, headers, url) do |response|
case response
when Net::HTTPNotModified
else
if output_path
File.open(output_path, "wb") do |output|
response.read_body do |chunk|
output.write(chunk)
end
end
last_modified = response["Last-Modified"]
if last_modified
FileUtils.touch(output_path,
mtime: Time.rfc2822(last_modified))
end
else
response.body
end
end
end
rescue Error => error
case error.response
when Net::HTTPNotFound
$stderr.puts(error.message)
return
else
raise
end
end
end
rescue
FileUtils.rm_f(output_path)
raise
end
end
def delete(path)
url = build_write_url(path)
with_retry(3, url) do
request(:delete, {}, url)
end
end
private
def build_request(method, url, headers, body: nil)
need_auth = false
case method
when :head
request = Net::HTTP::Head.new(url, headers)
when :get
request = Net::HTTP::Get.new(url, headers)
when :post
need_auth = true
request = Net::HTTP::Post.new(url, headers)
when :put
need_auth = true
request = Net::HTTP::Put.new(url, headers)
when :delete
need_auth = true
request = Net::HTTP::Delete.new(url, headers)
else
raise "unsupported HTTP method: #{method.inspect}"
end
request["Connection"] = "Keep-Alive"
setup_auth(request) if need_auth
if body
if body.is_a?(String)
request.body = body
else
request.body_stream = body
end
end
request
end
def with_retry(max_n_retries, target)
n_retries = 0
begin
yield
rescue Net::OpenTimeout,
OpenSSL::OpenSSLError,
SocketError,
SystemCallError,
Timeout::Error,
Error => error
n_retries += 1
if n_retries <= max_n_retries
$stderr.puts
$stderr.puts("Retry #{n_retries}: #{target}: " +
"#{error.class}: #{error.message}")
close
retry
else
raise
end
end
end
def with_read_timeout(timeout)
current_timeout, @current_timeout = @current_timeout, timeout
begin
yield
ensure
@current_timeout = current_timeout
end
end
end
# See also the REST API document:
# https://support.sonatype.com/hc/en-us/articles/213465868-Uploading-to-a-Nexus-Repository-2-Staging-Repository-via-REST-API
class MavenRepositoryClient < HTTPClient
PRODUCTION_DEPLOYED_BASE_URL =
"https://repo1.maven.org/maven2/org/apache/arrow"
STAGING_BASE_URL = "https://repository.apache.org"
STAGING_DEPLOYED_BASE_URL =
"#{STAGING_BASE_URL}/content/repositories/staging/org/apache/arrow"
STAGING_API_BASE_URL = "#{STAGING_BASE_URL}/service/local/staging"
def initialize(prefix, repository_id, asf_user, asf_password)
@prefix = prefix
@repository_id = repository_id
@asf_user = asf_user
@asf_password = asf_password
super()
end
def create_staging_repository(description="")
# The profile ID of "org.apache.arrow".
# See also: https://issues.apache.org/jira/browse/INFRA-26626
profile_id = "2653a12a1cbe8b"
url_string = "#{STAGING_API_BASE_URL}/profiles/#{profile_id}/start"
url = URI(url_string)
headers = {"Content-Type" => "application/xml"}
response = request(:post, headers, url, body: <<-REQUEST)
<promoteRequest>
<data>
<description>#{CGI.escape_html(description)}</description>
</data>
</promoteRequest>
REQUEST
response.body[/<stagedRepositoryId>(.+?)<\/stagedRepositoryId>/, 1]
end
def close_staging_repository(description="")
# The profile ID of "org.apache.arrow".
# See also: https://issues.apache.org/jira/browse/INFRA-26626
profile_id = "2653a12a1cbe8b"
url_string = "#{STAGING_API_BASE_URL}/profiles/#{profile_id}/finish"
url = URI(url_string)
headers = {"Content-Type" => "application/xml"}
response = request(:post, headers, url, body: <<-REQUEST)
<promoteRequest>
<data>
<stagedRepositoryId>#{CGI.escape_html(@repository_id)}</stagedRepositoryId>
<description>#{CGI.escape_html(description)}</description>
</data>
</promoteRequest>
REQUEST
response.body
end
def files
_files = []
directories = [""]
until directories.empty?
directory = directories.shift
list(directory).each do |path|
resolved_path = "#{directory}#{path}"
case path
when "../"
when /\/\z/
directories << resolved_path
else
_files << resolved_path
end
end
end
_files
end
def list(path)
url = build_deployed_url(path)
with_retry(3, url) do
begin
request(:get, {}, url) do |response|
response.body.scan(/<a href="(.+?)"/).flatten.collect do |href|
href.delete_prefix(url.to_s)
end
end
rescue Error => error
case error.response
when Net::HTTPNotFound
return []
else
raise
end
end
end
end
def upload(path, destination_path)
destination_url = build_api_url(destination_path)
with_retry(3, destination_url) do
headers = {
"Content-Length" => File.size(path).to_s,
"Content-Type" => content_type(path),
}
File.open(path, "rb") do |input|
request(:put, headers, destination_url, body: input)
end
end
end
private
def build_read_url(path)
build_deployed_url(path)
end
def build_write_url(path)
build_api_url(path)
end
def build_api_url(path)
url_string = STAGING_API_BASE_URL +
"/deployByRepositoryId/#{@repository_id}/org/apache/arrow" +
"/#{@prefix}/#{path}"
URI(url_string)
end
def build_deployed_url(path)
url_string = "#{PRODUCTION_DEPLOYED_BASE_URL}/#{@prefix}/#{path}"
URI(url_string)
end
def setup_auth(request)
request.basic_auth(@asf_user, @asf_password)
end
def content_type(path)
case File.extname(path)
when ".rpm"
"application/x-redhat-package-manager"
else
"application/octet-stream"
end
end
end
class ArtifactoryClient < HTTPClient
def initialize(prefix, api_key)
@prefix = prefix
@api_key = api_key
super()
end
def files
_files = []
directories = [""]
until directories.empty?
directory = directories.shift
list(directory).each do |path|
resolved_path = "#{directory}#{path}"
case path
when "../"
when /\/\z/
directories << resolved_path
else
_files << resolved_path
end
end
end
_files
end
def list(path)
url = build_deployed_url(path)
with_retry(3, url) do
begin
request(:get, {}, url) do |response|
response.body.scan(/<a href="(.+?)"/).flatten
end
rescue Error => error
case error.response
when Net::HTTPNotFound
return []
else
raise
end
end
end
end
def upload(path, destination_path)
destination_url = build_deployed_url(destination_path)
with_retry(3, destination_url) do
sha1 = Digest::SHA1.file(path).hexdigest
sha256 = Digest::SHA256.file(path).hexdigest
headers = {
"X-Artifactory-Last-Modified" => File.mtime(path).rfc2822,
"X-Checksum-Deploy" => "false",
"X-Checksum-Sha1" => sha1,
"X-Checksum-Sha256" => sha256,
"Content-Length" => File.size(path).to_s,
"Content-Type" => "application/octet-stream",
}
File.open(path, "rb") do |input|
request(:put, headers, destination_url, body: input)
end
end
end
def copy(source, destination)
url = build_api_url("copy/arrow/#{source}",
"to" => "/arrow/#{destination}")
with_retry(3, url) do
with_read_timeout(300) do
request(:post, {}, url)
end
end
end
private
def build_read_url(path)
build_deployed_url(path)
end
def build_write_url(path)
build_api_url(path, {})
end
def build_api_url(path, parameters)
uri_string = "https://packages.apache.org/artifactory/api/#{path}"
unless parameters.empty?
uri_string << "?"
escaped_parameters = parameters.collect do |key, value|
"#{CGI.escape(key)}=#{CGI.escape(value)}"
end
uri_string << escaped_parameters.join("&")
end
URI(uri_string)
end
def build_deployed_url(path)
uri_string = "https://packages.apache.org/artifactory/arrow"
uri_string << "/#{@prefix}" unless @prefix.nil?
uri_string << "/#{path}"
URI(uri_string)
end
def setup_auth(request)
request["X-JFrog-Art-Api"] = @api_key
end
end
class HTTPClientPool
class << self
def open(*args)
pool = new(*args)
begin
yield(pool)
ensure
pool.close
end
end
end
def initialize(*args)
@args = args
@mutex = Thread::Mutex.new
@clients = []
end
def pull
client = @mutex.synchronize do
if @clients.empty?
create_client
else
@clients.pop
end
end
begin
yield(client)
ensure
release(client)
end
end
def release(client)
@mutex.synchronize do
@clients << client
end
end
def close
@clients.each(&:close)
end
end
class MavenRepositoryClientPool < HTTPClientPool
private
def create_client
MavenRepositoryClient.new(*@args)
end
end
class ArtifactoryClientPool < HTTPClientPool
private
def create_client
ArtifactoryClient.new(*@args)
end
end
class Downloader
def download
progress_label = "Downloading: #{target_base_path}"
progress_reporter = ProgressReporter.new(progress_label)
prefix = [target_base_path, @prefix].compact.join("/")
open_client_pool(prefix) do |client_pool|
thread_pool = ThreadPool.new(thread_pool_use_case) do |path, output_path|
client_pool.pull do |client|
client.download(path, output_path)
end
progress_reporter.advance
end
files = client_pool.pull do |client|
client.files
end
if @target == :base and yum_repository?
# Download Yum repository metadata efficiently. We have many
# old unused repodata/*-.{sqlite,xml} files because we don't
# remove old unused repodata/*-.{sqlite,xml}. We want to
# download only used Yum repository metadata. We can find it
# by checking <location href="..."/> in repomd.xml.
dynamic_paths = []
files.each do |path|
next unless File.basename(path) == "repomd.xml"
output_path = "#{@destination}/#{path}"
yield(output_path)
output_dir = File.dirname(output_path)
FileUtils.mkdir_p(output_dir)
progress_reporter.increment_max
client_pool.pull do |client|
client.download(path, output_path)
end
progress_reporter.advance
base_dir = File.dirname(File.dirname(path))
File.read(output_path).scan(/<location\s+href="(.+?)"/) do |href,|
dynamic_paths << "#{base_dir}/#{href}"
end
end
else
dynamic_paths = nil
end
files.each do |path|
if @pattern
next unless @pattern.match?(path)
end
if dynamic_paths
next unless dynamic_paths.include?(path)
end
output_path = "#{@destination}/#{path}"
yield(output_path)
output_dir = File.dirname(output_path)
FileUtils.mkdir_p(output_dir)
progress_reporter.increment_max
thread_pool << [path, output_path]
end
thread_pool.join
end
progress_reporter.finish
end
private
def yum_repository?
case @distribution
when "almalinux", "amazon-linux", "centos"
true
else
false
end
end
end
class MavenRepositoryDownloader < Downloader
def initialize(asf_user:,
asf_password:,
destination:,
distribution:,
pattern: nil,
prefix: nil,
target: nil,
rc: nil)
@asf_user = asf_user
@asf_password = asf_password
@destination = destination
@distribution = distribution
@pattern = pattern
@prefix = prefix
@target = target
@rc = rc
end
private
def target_base_path
@distribution
end
def open_client_pool(prefix, &block)
args = [prefix, nil, @asf_user, @asf_password]
MavenRepositoryClientPool.open(*args, &block)
end
def thread_pool_use_case
:maven_repository
end
end
module ArtifactoryPath
private
def base_path
path = @distribution
path += "-staging" if @staging
path
end
def rc_base_path
base_path + "-rc"
end
def release_base_path
base_path
end
def target_base_path
if @rc
rc_base_path
else
release_base_path
end
end
end
class ArtifactoryDownloader < Downloader
include ArtifactoryPath
def initialize(api_key:,
destination:,
distribution:,
pattern: nil,
prefix: nil,
target: nil,
rc: nil,
staging: false)
@api_key = api_key
@destination = destination
@distribution = distribution
@pattern = pattern
@prefix = prefix
@target = target
if @target == :rc
@rc = rc
else
@rc = nil
end
@staging = staging
end
private
def open_client_pool(prefix, &block)
args = [prefix, @api_key]
ArtifactoryClientPool.open(*args, &block)
end
def thread_pool_use_case
:artifactory
end
end
class Uploader
def upload
progress_label = "Uploading: #{target_base_path}"
progress_reporter = ProgressReporter.new(progress_label)
prefix = target_base_path
prefix += "/#{@destination_prefix}" if @destination_prefix
open_client_pool(prefix) do |client_pool|
if @sync
existing_files = client_pool.pull do |client|
client.files
end
else
existing_files = []
end
thread_pool = ThreadPool.new(thread_pool_use_case) do |path, relative_path|
client_pool.pull do |client|
client.upload(path, relative_path)
end
progress_reporter.advance
end
source = Pathname(@source)
source.glob("**/*") do |path|
next if path.directory?
destination_path = path.relative_path_from(source)
progress_reporter.increment_max
existing_files.delete(destination_path.to_s)
thread_pool << [path, destination_path]
end
thread_pool.join
if @sync
thread_pool = ThreadPool.new(thread_pool_use_case) do |path|
client_pool.pull do |client|
client.delete(path)
end
progress_reporter.advance
end
existing_files.each do |path|
if @sync_pattern
next unless @sync_pattern.match?(path)
end
progress_reporter.increment_max
thread_pool << path
end
thread_pool.join
end
end
progress_reporter.finish
end
end
class MavenRepositoryUploader < Uploader
def initialize(asf_user:,
asf_password:,
destination_prefix: nil,
distribution:,
rc: nil,
source:,
staging: false,
sync: false,
sync_pattern: nil)
@asf_user = asf_user
@asf_password = asf_password
@destination_prefix = destination_prefix
@distribution = distribution
@rc = rc
@source = source
@staging = staging
@sync = sync
@sync_pattern = sync_pattern
end
def upload
client = MavenRepositoryClient.new(nil, nil, @asf_user, @asf_password)
@repository_id = client.create_staging_repository
super
client = MavenRepositoryClient.new(nil,
@repository_id,
@asf_user,
@asf_password)
client.close_staging_repository
end
private
def target_base_path
@distribution
end
def open_client_pool(prefix, &block)
args = [prefix, @repository_id, @asf_user, @asf_password]
MavenRepositoryClientPool.open(*args, &block)
end
def thread_pool_use_case
:maven_repository
end
end
class ArtifactoryUploader < Uploader
include ArtifactoryPath
def initialize(api_key:,
destination_prefix: nil,
distribution:,
rc: nil,
source:,
staging: false,
sync: false,
sync_pattern: nil)
@api_key = api_key
@destination_prefix = destination_prefix
@distribution = distribution
@rc = rc
@source = source
@staging = staging
@sync = sync
@sync_pattern = sync_pattern
end
private
def open_client_pool(prefix, &block)
args = [prefix, @api_key]
ArtifactoryClientPool.open(*args, &block)
end
def thread_pool_use_case
:artifactory
end
end
class ArtifactoryReleaser
include ArtifactoryPath
def initialize(api_key:,
distribution:,
list: nil,
rc_prefix: nil,
release_prefix: nil,
staging: false)
@api_key = api_key
@distribution = distribution
@list = list
@rc_prefix = rc_prefix
@release_prefix = release_prefix
@staging = staging
end
def release
progress_label = "Releasing: #{release_base_path}"
progress_reporter = ProgressReporter.new(progress_label)
rc_prefix = [rc_base_path, @rc_prefix].compact.join("/")
release_prefix = [release_base_path, @release_prefix].compact.join("/")
ArtifactoryClientPool.open(rc_prefix, @api_key) do |client_pool|
thread_pool = ThreadPool.new(:artifactory) do |path, release_path|
client_pool.pull do |client|
client.copy(path, release_path)
end
progress_reporter.advance
end
files = client_pool.pull do |client|
if @list
client.download(@list, nil).lines(chomp: true)
else
client.files
end
end
files.each do |path|
progress_reporter.increment_max
rc_path = "#{rc_prefix}/#{path}"
release_path = "#{release_prefix}/#{path}"
thread_pool << [rc_path, release_path]
end
thread_pool.join
end
progress_reporter.finish
end
end
def define
define_apt_tasks
define_yum_tasks
define_r_tasks
define_summary_tasks
end
private
def env_value(name)
value = ENV[name]
value = yield(name) if value.nil? and block_given?
raise "Specify #{name} environment variable" if value.nil?
value
end
def verbose?
ENV["VERBOSE"] == "yes"
end
def default_output
if verbose?
$stdout
else
IO::NULL
end
end
def gpg_key_id
env_value("GPG_KEY_ID")
end
def shorten_gpg_key_id(id)
id[-8..-1]
end
def rpm_gpg_key_package_name(id)
"gpg-pubkey-#{shorten_gpg_key_id(id).downcase}"
end
def artifactory_api_key
env_value("ARTIFACTORY_API_KEY")
end
def asf_user
env_value("ASF_USER")
end
def asf_password
env_value("ASF_PASSWORD")
end
def artifacts_dir
env_value("ARTIFACTS_DIR")
end
def version
env_value("VERSION")
end
def rc
env_value("RC")
end
def staging?
ENV["STAGING"] == "yes"
end
def full_version
"#{version}-rc#{rc}"
end
def valid_sign?(path, sign_path)
IO.pipe do |input, output|
begin
sh({"LANG" => "C"},
"gpg",
"--verify",
sign_path,
path,
out: default_output,
err: output,
verbose: false)
rescue
return false
end
output.close
/Good signature/ === input.read
end
end
def sign(source_path, destination_path)
if File.exist?(destination_path)
return if valid_sign?(source_path, destination_path)
rm(destination_path, verbose: false)
end
sh("gpg",
"--armor",
"--detach-sign",
"--local-user", gpg_key_id,
"--output", destination_path,
source_path,
out: default_output,
verbose: verbose?)
end
def sha512(source_path, destination_path)
if File.exist?(destination_path)
sha512 = File.read(destination_path).split[0]
return if Digest::SHA512.file(source_path).hexdigest == sha512
end
absolute_destination_path = File.expand_path(destination_path)
Dir.chdir(File.dirname(source_path)) do
sh("shasum",
"--algorithm", "512",
File.basename(source_path),
out: absolute_destination_path,
verbose: verbose?)
end
end
def sign_dir(label, dir)
progress_label = "Signing: #{label}"
progress_reporter = ProgressReporter.new(progress_label)
target_paths = []
Pathname(dir).glob("**/*") do |path|
next if path.directory?
case path.extname
when ".asc", ".sha512"
next
end
progress_reporter.increment_max
target_paths << path.to_s
end
target_paths.each do |path|
sign(path, "#{path}.asc")
sha512(path, "#{path}.sha512")
progress_reporter.advance
end
progress_reporter.finish
end
def download_distribution(type,
distribution,
destination,
target,
pattern: nil,
prefix: nil)
mkdir_p(destination, verbose: verbose?) unless File.exist?(destination)
existing_paths = {}
Pathname(destination).glob("**/*") do |path|
next if path.directory?
existing_paths[path.to_s] = true
end
options = {
destination: destination,
distribution: distribution,
pattern: pattern,
prefix: prefix,
target: target,
}
options[:rc] = rc if target == :rc
if type == :artifactory
options[:api_key] = artifactory_api_key
options[:staging] = staging?
downloader = ArtifactoryDownloader.new(**options)
else
options[:asf_user] = asf_user
options[:asf_password] = asf_password
downloader = MavenRepositoryDownloader.new(**options)
end
downloader.download do |output_path|
existing_paths.delete(output_path)
end
existing_paths.each_key do |path|
rm_f(path, verbose: verbose?)
end
end
def release_distribution(distribution,
list: nil,
rc_prefix: nil,
release_prefix: nil)
options = {
api_key: artifactory_api_key,
distribution: distribution,
list: list,
rc_prefix: rc_prefix,
release_prefix: release_prefix,
staging: staging?,
}
releaser = ArtifactoryReleaser.new(**options)
releaser.release
end
def same_content?(path1, path2)
File.exist?(path1) and
File.exist?(path2) and
Digest::SHA256.file(path1) == Digest::SHA256.file(path2)
end
def copy_artifact(source_path,
destination_path,
progress_reporter)
return if same_content?(source_path, destination_path)
progress_reporter.increment_max
destination_dir = File.dirname(destination_path)
unless File.exist?(destination_dir)
mkdir_p(destination_dir, verbose: verbose?)
end
cp(source_path, destination_path, verbose: verbose?)
progress_reporter.advance
end
def prepare_staging(base_path)
client = ArtifactoryClient.new(nil, artifactory_api_key)
["", "-rc"].each do |suffix|
path = "#{base_path}#{suffix}"
progress_reporter = ProgressReporter.new("Preparing staging for #{path}")
progress_reporter.increment_max
begin
staging_path = "#{base_path}-staging#{suffix}"
if client.exist?(staging_path)
client.delete(staging_path)
end
if client.exist?(path)
client.copy(path, staging_path)
end
ensure
progress_reporter.advance
progress_reporter.finish
end
end
end
def delete_staging(base_path)
client = ArtifactoryClient.new(nil, artifactory_api_key)
["", "-rc"].each do |suffix|
path = "#{base_path}#{suffix}"
progress_reporter = ProgressReporter.new("Deleting staging for #{path}")
progress_reporter.increment_max
begin
staging_path = "#{base_path}-staging#{suffix}"
if client.exist?(staging_path)
client.delete(staging_path)
end
ensure
progress_reporter.advance
progress_reporter.finish
end
end
end
def uploaded_files_name
"uploaded-files.txt"
end
def write_uploaded_files(dir)
dir = Pathname(dir)
uploaded_files = []
dir.glob("**/*") do |path|
next if path.directory?
uploaded_files << path.relative_path_from(dir).to_s
end
File.open("#{dir}/#{uploaded_files_name}", "w") do |output|
output.puts(uploaded_files.sort)
end
end
def tmp_dir
"/tmp"
end
def rc_dir
"#{tmp_dir}/rc"
end
def release_dir
"#{tmp_dir}/release"
end
def recover_dir
"#{tmp_dir}/recover"
end
def apt_repository_label
"Apache Arrow"
end
def apt_repository_description
"Apache Arrow packages"
end
def apt_rc_repositories_dir
"#{rc_dir}/apt/repositories"
end
def apt_recover_repositories_dir
"#{recover_dir}/apt/repositories"
end
def available_apt_targets
[
["debian", "bookworm", "main"],
["debian", "trixie", "main"],
["ubuntu", "jammy", "main"],
["ubuntu", "noble", "main"],
]
end
def apt_targets
env_apt_targets = (ENV["APT_TARGETS"] || "").split(",")
if env_apt_targets.empty?
available_apt_targets
else
available_apt_targets.select do |distribution, code_name, component|
env_apt_targets.any? do |env_apt_target|
if env_apt_target.include?("-")
env_apt_target.start_with?("#{distribution}-#{code_name}")
else
env_apt_target == distribution
end
end
end
end
end
def apt_distributions
apt_targets.collect(&:first).uniq
end
def apt_architectures
[
"amd64",
"arm64",
]
end
def generate_apt_release(dists_dir, code_name, component, architecture)
dir = "#{dists_dir}/#{component}/"
if architecture == "source"
dir << architecture
else
dir << "binary-#{architecture}"
end
mkdir_p(dir, verbose: verbose?)
File.open("#{dir}/Release", "w") do |release|
release.puts(<<-RELEASE)
Archive: #{code_name}
Component: #{component}
Origin: #{apt_repository_label}
Label: #{apt_repository_label}
Architecture: #{architecture}
RELEASE
end
end
def generate_apt_ftp_archive_generate_conf(code_name, component)
conf = <<-CONF
Dir::ArchiveDir ".";
Dir::CacheDir ".";
TreeDefault::Directory "pool/#{code_name}/#{component}";
TreeDefault::SrcDirectory "pool/#{code_name}/#{component}";
Default::Packages::Extensions ".deb .ddeb";
Default::Packages::Compress ". gzip xz";
Default::Sources::Compress ". gzip xz";
Default::Contents::Compress "gzip";
CONF
apt_architectures.each do |architecture|
conf << <<-CONF
BinDirectory "dists/#{code_name}/#{component}/binary-#{architecture}" {
Packages "dists/#{code_name}/#{component}/binary-#{architecture}/Packages";
Contents "dists/#{code_name}/#{component}/Contents-#{architecture}";
SrcPackages "dists/#{code_name}/#{component}/source/Sources";
};
CONF
end
conf << <<-CONF
Tree "dists/#{code_name}" {
Sections "#{component}";
Architectures "#{apt_architectures.join(" ")} source";
};
CONF
conf
end
def generate_apt_ftp_archive_release_conf(code_name, component)
<<-CONF
APT::FTPArchive::Release::Origin "#{apt_repository_label}";
APT::FTPArchive::Release::Label "#{apt_repository_label}";
APT::FTPArchive::Release::Architectures "#{apt_architectures.join(" ")}";
APT::FTPArchive::Release::Codename "#{code_name}";
APT::FTPArchive::Release::Suite "#{code_name}";
APT::FTPArchive::Release::Components "#{component}";
APT::FTPArchive::Release::Description "#{apt_repository_description}";
CONF
end
def apt_update(base_dir, incoming_dir, merged_dir)
apt_targets.each do |distribution, code_name, component|
distribution_dir = "#{incoming_dir}/#{distribution}"
pool_dir = "#{distribution_dir}/pool/#{code_name}"
next unless File.exist?(pool_dir)
dists_dir = "#{distribution_dir}/dists/#{code_name}"
rm_rf(dists_dir, verbose: verbose?)
generate_apt_release(dists_dir, code_name, component, "source")
apt_architectures.each do |architecture|
generate_apt_release(dists_dir, code_name, component, architecture)
end
generate_conf_file = Tempfile.new("apt-ftparchive-generate.conf")
File.open(generate_conf_file.path, "w") do |conf|
conf.puts(generate_apt_ftp_archive_generate_conf(code_name,
component))
end
cd(distribution_dir, verbose: verbose?) do
sh("apt-ftparchive",
"generate",
generate_conf_file.path,
out: default_output,
verbose: verbose?)
end
Dir.glob("#{dists_dir}/Release*") do |release|
rm_f(release, verbose: verbose?)
end
Dir.glob("#{distribution_dir}/*.db") do |db|
rm_f(db, verbose: verbose?)
end
release_conf_file = Tempfile.new("apt-ftparchive-release.conf")
File.open(release_conf_file.path, "w") do |conf|
conf.puts(generate_apt_ftp_archive_release_conf(code_name,
component))
end
release_file = Tempfile.new("apt-ftparchive-release")
sh("apt-ftparchive",
"-c", release_conf_file.path,
"release",
dists_dir,
out: release_file.path,
verbose: verbose?)
mv(release_file.path, "#{dists_dir}/Release", verbose: verbose?)
if base_dir and merged_dir
base_dists_dir = "#{base_dir}/#{distribution}/dists/#{code_name}"
merged_dists_dir = "#{merged_dir}/#{distribution}/dists/#{code_name}"
rm_rf(merged_dists_dir)
merger = APTDistsMerge::Merger.new(base_dists_dir,
dists_dir,
merged_dists_dir)
merger.merge
in_release_path = "#{merged_dists_dir}/InRelease"
release_path = "#{merged_dists_dir}/Release"
else
in_release_path = "#{dists_dir}/InRelease"
release_path = "#{dists_dir}/Release"
end
signed_release_path = "#{release_path}.gpg"
sh("gpg",
"--sign",
"--detach-sign",
"--armor",
"--local-user", gpg_key_id,
"--output", signed_release_path,
release_path,
out: default_output,
verbose: verbose?)
sh("gpg",
"--clear-sign",
"--local-user", gpg_key_id,
"--output", in_release_path,
release_path,
out: default_output,
verbose: verbose?)
end
end
def define_apt_staging_tasks
namespace :apt do
namespace :staging do
desc "Prepare staging environment for APT repositories"
task :prepare do
apt_distributions.each do |distribution|
prepare_staging(distribution)
end
end
desc "Delete staging environment for APT repositories"
task :delete do
apt_distributions.each do |distribution|
delete_staging(distribution)
end
end
end
end
end
def define_apt_rc_tasks
namespace :apt do
namespace :rc do
base_dir = "#{apt_rc_repositories_dir}/base"
incoming_dir = "#{apt_rc_repositories_dir}/incoming"
merged_dir = "#{apt_rc_repositories_dir}/merged"
upload_dir = "#{apt_rc_repositories_dir}/upload"
desc "Copy .deb packages"
task :copy do
apt_targets.each do |distribution, code_name, component|
progress_label = "Copying: #{distribution} #{code_name}"
progress_reporter = ProgressReporter.new(progress_label)
distribution_dir = "#{incoming_dir}/#{distribution}"
pool_dir = "#{distribution_dir}/pool/#{code_name}"
rm_rf(pool_dir, verbose: verbose?)
mkdir_p(pool_dir, verbose: verbose?)
source_dir_prefix = "#{artifacts_dir}/#{distribution}-#{code_name}"
# apache/arrow uses debian-bookworm-{amd64,arm64} but
# apache/arrow-adbc uses debian-bookworm. So the following
# glob must much both of them.
Dir.glob("#{source_dir_prefix}*/*") do |path|
base_name = File.basename(path)
package_name = ENV["DEB_PACKAGE_NAME"]
if package_name.nil? or package_name.empty?
if base_name.start_with?("apache-arrow-apt-source")
package_name = "apache-arrow-apt-source"
else
package_name = "apache-arrow"
end
end
destination_path = [
pool_dir,
component,
package_name[0],
package_name,
base_name,
].join("/")
copy_artifact(path,
destination_path,
progress_reporter)
case base_name
when /\A[^_]+-apt-source_.*\.deb\z/
latest_apt_source_package_path = [
distribution_dir,
"#{package_name}-latest-#{code_name}.deb"
].join("/")
copy_artifact(path,
latest_apt_source_package_path,
progress_reporter)
end
end
progress_reporter.finish
end
end
desc "Download dists/ for RC APT repositories"
task :download do
apt_targets.each do |distribution, code_name, component|
not_checksum_pattern = /.+(?<!\.asc|\.sha512)\z/
base_distribution_dir =
"#{base_dir}/#{distribution}/dists/#{code_name}"
pattern = not_checksum_pattern
download_distribution(:artifactory,
distribution,
base_distribution_dir,
:base,
pattern: pattern,
prefix: "dists/#{code_name}")
end
end
desc "Sign .deb packages"
task :sign do
apt_distributions.each do |distribution|
distribution_dir = "#{incoming_dir}/#{distribution}"
Dir.glob("#{distribution_dir}/**/*.dsc") do |path|
begin
sh({"LANG" => "C"},
"gpg",
"--verify",
path,
out: IO::NULL,
err: IO::NULL,
verbose: false)
rescue
sh("debsign",
"--no-re-sign",
"-k#{gpg_key_id}",
path,
out: default_output,
verbose: verbose?)
end
end
sign_dir(distribution, distribution_dir)
end
end
desc "Update RC APT repositories"
task :update do
apt_update(base_dir, incoming_dir, merged_dir)
apt_targets.each do |distribution, code_name, component|
dists_dir = "#{merged_dir}/#{distribution}/dists/#{code_name}"
next unless File.exist?(dists_dir)
sign_dir("#{distribution} #{code_name}",
dists_dir)
end
end
desc "Upload .deb packages and RC APT repositories"
task :upload do
apt_distributions.each do |distribution|
upload_distribution_dir = "#{upload_dir}/#{distribution}"
incoming_distribution_dir = "#{incoming_dir}/#{distribution}"
merged_dists_dir = "#{merged_dir}/#{distribution}/dists"
rm_rf(upload_distribution_dir, verbose: verbose?)
mkdir_p(upload_distribution_dir, verbose: verbose?)
Dir.glob("#{incoming_distribution_dir}/*") do |path|
next if File.basename(path) == "dists"
cp_r(path,
upload_distribution_dir,
preserve: true,
verbose: verbose?)
end
cp_r(merged_dists_dir,
upload_distribution_dir,
preserve: true,
verbose: verbose?)
write_uploaded_files(upload_distribution_dir)
uploader = ArtifactoryUploader.new(api_key: artifactory_api_key,
distribution: distribution,
rc: rc,
source: upload_distribution_dir,
staging: staging?)
uploader.upload
end
end
end
desc "Release RC APT repositories"
apt_rc_tasks = [
"apt:rc:copy",
"apt:rc:download",
"apt:rc:sign",
"apt:rc:update",
"apt:rc:upload",
]
apt_rc_tasks.unshift("apt:staging:prepare") if staging?
task :rc => apt_rc_tasks
end
end
def define_apt_release_tasks
namespace :apt do
desc "Release APT repository"
task :release do
apt_distributions.each do |distribution|
release_distribution(distribution,
list: uploaded_files_name)
end
end
end
end
def define_apt_recover_tasks
namespace :apt do
namespace :recover do
desc "Download repositories"
task :download do
apt_targets.each do |distribution, code_name, component|
not_checksum_pattern = /.+(?<!\.asc|\.sha512)\z/
code_name_dir =
"#{apt_recover_repositories_dir}/#{distribution}/pool/#{code_name}"
pattern = not_checksum_pattern
download_distribution(:artifactory,
distribution,
code_name_dir,
:base,
pattern: pattern,
prefix: "pool/#{code_name}")
end
end
desc "Update repositories"
task :update do
apt_update(nil, apt_recover_repositories_dir, nil)
apt_targets.each do |distribution, code_name, component|
dists_dir =
"#{apt_recover_repositories_dir}/#{distribution}/dists/#{code_name}"
next unless File.exist?(dists_dir)
sign_dir("#{distribution} #{code_name}",
dists_dir)
end
end
desc "Upload repositories"
task :upload do
apt_distributions.each do |distribution|
dists_dir =
"#{apt_recover_repositories_dir}/#{distribution}/dists"
uploader = ArtifactoryUploader.new(api_key: artifactory_api_key,
destination_prefix: "dists",
distribution: distribution,
source: dists_dir,
staging: staging?)
uploader.upload
end
end
end
desc "Recover APT repositories"
apt_recover_tasks = [
"apt:recover:download",
"apt:recover:update",
"apt:recover:upload",
]
task :recover => apt_recover_tasks
end
end
def define_apt_tasks
define_apt_staging_tasks
define_apt_rc_tasks
define_apt_release_tasks
define_apt_recover_tasks
end
def yum_rc_repositories_dir
"#{rc_dir}/yum/repositories"
end
def yum_release_repositories_dir
"#{release_dir}/yum/repositories"
end
def available_yum_targets
[
["almalinux", "9"],
["almalinux", "8"],
["amazon-linux", "2023"],
["centos", "9-stream"],
["centos", "8-stream"],
["centos", "7"],
]
end
def yum_targets
env_yum_targets = (ENV["YUM_TARGETS"] || "").split(",")
if env_yum_targets.empty?
available_yum_targets
else
available_yum_targets.select do |distribution, distribution_version|
env_yum_targets.any? do |env_yum_target|
if /\d/.match?(env_yum_target)
env_yum_target.start_with?("#{distribution}-#{distribution_version}")
else
env_yum_target == distribution
end
end
end
end
end
def yum_distributions
yum_targets.collect(&:first).uniq
end
def yum_architectures
[
"aarch64",
"x86_64",
]
end
def signed_rpm?(rpm)
IO.pipe do |input, output|
system("rpm", "--checksig", rpm, out: output)
output.close
signature = input.gets.sub(/\A#{Regexp.escape(rpm)}: /, "")
signature.split.include?("signatures")
end
end
def sign_rpms(directory)
thread_pool = ThreadPool.new(:gpg) do |rpm|
unless signed_rpm?(rpm)
sh("rpm",
"-D", "_gpg_name #{gpg_key_id}",
"-D", "__gpg /usr/bin/gpg",
"-D", "__gpg_check_password_cmd /bin/true true",
"--resign",
rpm,
out: default_output,
verbose: verbose?)
end
end
Dir.glob("#{directory}/**/*.rpm") do |rpm|
thread_pool << rpm
end
thread_pool.join
end
def rpm_sign(directory)
unless system("rpm", "-q",
rpm_gpg_key_package_name(gpg_key_id),
out: IO::NULL)
gpg_key = Tempfile.new(["apache-arrow-binary", ".asc"])
sh("gpg",
"--armor",
"--export", gpg_key_id,
out: gpg_key.path,
verbose: verbose?)
sh("rpm",
"--import", gpg_key.path,
out: default_output,
verbose: verbose?)
gpg_key.close!
end
yum_targets.each do |distribution, distribution_version|
source_dir = [
directory,
distribution,
distribution_version,
].join("/")
sign_rpms(source_dir)
end
end
def yum_update(base_dir, incoming_dir)
yum_targets.each do |distribution, distribution_version|
target_dir = "#{incoming_dir}/#{distribution}/#{distribution_version}"
target_dir = Pathname(target_dir)
next unless target_dir.directory?
base_target_dir = Pathname(base_dir) + distribution + distribution_version
if base_target_dir.exist?
base_target_dir.glob("*") do |base_arch_dir|
next unless base_arch_dir.directory?
base_repodata_dir = base_arch_dir + "repodata"
next unless base_repodata_dir.exist?
target_repodata_dir = target_dir + base_arch_dir.basename + "repodata"
rm_rf(target_repodata_dir, verbose: verbose?)
mkdir_p(target_repodata_dir.parent, verbose: verbose?)
cp_r(base_repodata_dir,
target_repodata_dir,
preserve: true,
verbose: verbose?)
end
end
target_dir.glob("*") do |arch_dir|
next unless arch_dir.directory?
packages = Tempfile.new("createrepo-c-packages")
Pathname.glob("#{arch_dir}/*/*.rpm") do |rpm|
relative_rpm = rpm.relative_path_from(arch_dir)
packages.puts(relative_rpm.to_s)
end
packages.close
sh("createrepo_c",
"--pkglist", packages.path,
"--recycle-pkglist",
"--retain-old-md-by-age=0",
"--skip-stat",
"--update",
arch_dir.to_s,
out: default_output,
verbose: verbose?)
end
end
end
def define_yum_staging_tasks
namespace :yum do
namespace :staging do
desc "Prepare staging environment for Yum repositories"
task :prepare do
yum_distributions.each do |distribution|
prepare_staging(distribution)
end
end
desc "Delete staging environment for Yum repositories"
task :delete do
yum_distributions.each do |distribution|
delete_staging(distribution)
end
end
end
end
end
def define_yum_rc_tasks
namespace :yum do
namespace :rc do
base_dir = "#{yum_rc_repositories_dir}/base"
incoming_dir = "#{yum_rc_repositories_dir}/incoming"
upload_dir = "#{yum_rc_repositories_dir}/upload"
desc "Copy RPM packages"
task :copy do
yum_targets.each do |distribution, distribution_version|
progress_label = "Copying: #{distribution} #{distribution_version}"
progress_reporter = ProgressReporter.new(progress_label)
destination_prefix = [
incoming_dir,
distribution,
distribution_version,
].join("/")
rm_rf(destination_prefix, verbose: verbose?)
source_dir_prefix =
"#{artifacts_dir}/#{distribution}-#{distribution_version}"
Dir.glob("#{source_dir_prefix}*/*.rpm") do |path|
base_name = File.basename(path)
type = base_name.split(".")[-2]
destination_paths = []
case type
when "src"
destination_paths << [
destination_prefix,
"Source",
"SPackages",
base_name,
].join("/")
when "noarch"
yum_architectures.each do |architecture|
destination_paths << [
destination_prefix,
architecture,
"Packages",
base_name,
].join("/")
end
else
destination_paths << [
destination_prefix,
type,
"Packages",
base_name,
].join("/")
end
destination_paths.each do |destination_path|
copy_artifact(path,
destination_path,
progress_reporter)
end
case base_name
when /\A(apache-arrow-release)-.*\.noarch\.rpm\z/
package_name = $1
latest_release_package_path = [
destination_prefix,
"#{package_name}-latest.rpm"
].join("/")
copy_artifact(path,
latest_release_package_path,
progress_reporter)
end
end
progress_reporter.finish
end
end
desc "Download repodata for RC Yum repositories"
task :download do
yum_distributions.each do |distribution|
distribution_dir = "#{base_dir}/#{distribution}"
download_distribution(:artifactory,
distribution,
distribution_dir,
:base,
pattern: /\/repodata\//)
end
end
desc "Sign RPM packages"
task :sign do
rpm_sign(incoming_dir)
yum_targets.each do |distribution, distribution_version|
source_dir = [
incoming_dir,
distribution,
distribution_version,
].join("/")
sign_dir("#{distribution}-#{distribution_version}",
source_dir)
end
end
desc "Update RC Yum repositories"
task :update do
yum_update(base_dir, incoming_dir)
yum_targets.each do |distribution, distribution_version|
target_dir = [
incoming_dir,
distribution,
distribution_version,
].join("/")
target_dir = Pathname(target_dir)
next unless target_dir.directory?
target_dir.glob("*") do |arch_dir|
next unless arch_dir.directory?
sign_label =
"#{distribution}-#{distribution_version} #{arch_dir.basename}"
sign_dir(sign_label,
arch_dir.to_s)
end
end
end
desc "Upload RC Yum repositories"
task :upload => yum_rc_repositories_dir do
yum_distributions.each do |distribution|
incoming_target_dir = "#{incoming_dir}/#{distribution}"
upload_target_dir = "#{upload_dir}/#{distribution}"
rm_rf(upload_target_dir, verbose: verbose?)
mkdir_p(upload_target_dir, verbose: verbose?)
cp_r(Dir.glob("#{incoming_target_dir}/*"),
upload_target_dir.to_s,
preserve: true,
verbose: verbose?)
write_uploaded_files(upload_target_dir)
uploader = ArtifactoryUploader.new(api_key: artifactory_api_key,
distribution: distribution,
rc: rc,
source: upload_target_dir,
staging: staging?,
# Don't remove old repodata
# because our implementation
# doesn't support it.
sync: false,
sync_pattern: /\/repodata\//)
uploader.upload
end
end
end
desc "Release RC Yum packages"
yum_rc_tasks = [
"yum:rc:copy",
"yum:rc:download",
"yum:rc:sign",
"yum:rc:update",
"yum:rc:upload",
]
yum_rc_tasks.unshift("yum:staging:prepare") if staging?
task :rc => yum_rc_tasks
end
end
def define_yum_release_tasks
directory yum_release_repositories_dir
namespace :yum do
desc "Release Yum packages"
task :release => yum_release_repositories_dir do
yum_distributions.each do |distribution|
release_distribution(distribution,
list: uploaded_files_name)
distribution_dir = "#{yum_release_repositories_dir}/#{distribution}"
download_distribution(:artifactory,
distribution,
distribution_dir,
:rc,
pattern: /\/repodata\//)
uploader = ArtifactoryUploader.new(api_key: artifactory_api_key,
distribution: distribution,
source: distribution_dir,
staging: staging?,
# Don't remove old repodata for
# unsupported distribution version
# such as Amazon Linux 2.
# This keeps garbage in repodata/
# for currently available
# distribution versions but we
# accept it for easy to implement.
sync: false,
sync_pattern: /\/repodata\//)
uploader.upload
end
end
end
end
def define_yum_tasks
define_yum_staging_tasks
define_yum_rc_tasks
define_yum_release_tasks
end
def define_generic_data_rc_tasks(label,
id,
rc_dir,
target_files_glob)
directory rc_dir
namespace id do
namespace :rc do
desc "Copy #{label} packages"
task :copy => rc_dir do
progress_label = "Copying: #{label}"
progress_reporter = ProgressReporter.new(progress_label)
Pathname(artifacts_dir).glob(target_files_glob) do |path|
next if path.directory?
destination_path = [
rc_dir,
path.basename.to_s,
].join("/")
copy_artifact(path, destination_path, progress_reporter)
end
progress_reporter.finish
end
desc "Sign #{label} packages"
task :sign => rc_dir do
sign_dir(label, rc_dir)
end
desc "Upload #{label} packages"
task :upload do
uploader =
ArtifactoryUploader.new(api_key: artifactory_api_key,
destination_prefix: full_version,
distribution: id.to_s,
rc: rc,
source: rc_dir,
staging: staging?)
uploader.upload
end
end
desc "Release RC #{label} packages"
rc_tasks = [
"#{id}:rc:copy",
"#{id}:rc:sign",
"#{id}:rc:upload",
]
task :rc => rc_tasks
end
end
def define_generic_data_release_tasks(label, id, release_dir)
directory release_dir
namespace id do
desc "Release #{label} packages"
task :release do
release_distribution(id.to_s,
rc_prefix: full_version,
release_prefix: version)
end
end
end
def define_generic_data_tasks(label,
id,
rc_dir,
release_dir,
target_files_glob)
define_generic_data_rc_tasks(label, id, rc_dir, target_files_glob)
define_generic_data_release_tasks(label, id, release_dir)
end
def define_r_rc_tasks(label, id, rc_dir)
directory rc_dir
namespace id do
namespace :rc do
desc "Prepare #{label} packages"
task :prepare => rc_dir do
progress_label = "Preparing #{label}"
progress_reporter = ProgressReporter.new(progress_label)
pattern = "r-binary-packages/r-lib*.{zip,tgz}"
Pathname(artifacts_dir).glob(pattern) do |path|
destination_path = [
rc_dir,
# r-lib__libarrow__bin__centos-7__arrow-8.0.0.zip
# --> libarrow/bin/centos-7/arrow-8.0.0.zip
path.basename.to_s.gsub(/\Ar-lib__/, "").gsub(/__/, "/"),
].join("/")
copy_artifact(path, destination_path, progress_reporter)
end
progress_reporter.finish
end
desc "Sign #{label} packages"
task :sign => rc_dir do
sign_dir(label, rc_dir)
end
desc "Upload #{label} packages"
task :upload do
uploader =
ArtifactoryUploader.new(api_key: artifactory_api_key,
destination_prefix: full_version,
distribution: id.to_s,
rc: rc,
source: rc_dir,
staging: staging?)
uploader.upload
end
end
desc "Release RC #{label} packages"
rc_tasks = [
"#{id}:rc:prepare",
"#{id}:rc:sign",
"#{id}:rc:upload",
]
task :rc => rc_tasks
end
end
def define_r_tasks
label = "R"
id = :r
r_rc_dir = "#{rc_dir}/r/#{full_version}"
r_release_dir = "#{release_dir}/r/#{full_version}"
define_r_rc_tasks(label, id, r_rc_dir)
define_generic_data_release_tasks(label, id, r_release_dir)
end
def define_summary_tasks
namespace :summary do
desc "Show RC summary"
task :rc do
suffix = ""
suffix << "-staging" if staging?
puts(<<-SUMMARY)
Success! The release candidate binaries are available here:
https://packages.apache.org/artifactory/arrow/almalinux#{suffix}-rc/
https://packages.apache.org/artifactory/arrow/amazon-linux#{suffix}-rc/
https://packages.apache.org/artifactory/arrow/centos#{suffix}-rc/
https://packages.apache.org/artifactory/arrow/debian#{suffix}-rc/
https://packages.apache.org/artifactory/arrow/ubuntu#{suffix}-rc/
SUMMARY
end
desc "Show release summary"
task :release do
suffix = ""
suffix << "-staging" if staging?
puts(<<-SUMMARY)
Success! The release binaries are available here:
https://packages.apache.org/artifactory/arrow/almalinux#{suffix}/
https://packages.apache.org/artifactory/arrow/amazon-linux#{suffix}/
https://packages.apache.org/artifactory/arrow/centos#{suffix}/
https://packages.apache.org/artifactory/arrow/debian#{suffix}/
https://packages.apache.org/artifactory/arrow/ubuntu#{suffix}/
SUMMARY
end
end
end
end
class LocalBinaryTask < BinaryTask
def initialize(packages, top_source_directory)
@packages = packages
@top_source_directory = top_source_directory
super()
end
def define
define_apt_test_task
define_yum_test_task
end
private
def resolve_docker_image(target)
case target
when /-(?:arm64|aarch64)\z/
target = Regexp.last_match.pre_match
platform = "linux/arm64"
else
platform = "linux/amd64"
end
case target
when /\Acentos-(\d+)-stream\z/
centos_stream_version = $1
image = "quay.io/centos/centos:stream#{centos_stream_version}"
else
case platform
when "linux/arm64"
image = "arm64v8/"
else
image = ""
end
target = target.gsub(/\Aamazon-linux/, "amazonlinux")
image << target.gsub(/-/, ":")
end
[platform, image]
end
def verify_apt_sh
"/host/dev/release/verify-apt.sh"
end
def verify_yum_sh
"/host/dev/release/verify-yum.sh"
end
def verify(target)
verify_command_line = [
"docker",
"run",
"--log-driver", "none",
"--rm",
"--security-opt", "seccomp=unconfined",
"--volume", "#{@top_source_directory}:/host:delegated",
]
if $stdin.tty?
verify_command_line << "--interactive"
verify_command_line << "--tty"
else
verify_command_line.concat(["--attach", "STDOUT"])
verify_command_line.concat(["--attach", "STDERR"])
end
platform, docker_image = resolve_docker_image(target)
docker_info = JSON.parse(`docker info --format '{{json .}}'`)
case [platform, docker_info["Architecture"]]
when ["linux/amd64", "x86_64"],
["linux/arm64", "aarch64"]
# Do nothing
else
verify_command_line.concat(["--platform", platform])
end
verify_command_line << docker_image
case target
when /\Adebian-/, /\Aubuntu-/
verify_command_line << verify_apt_sh
else
verify_command_line << verify_yum_sh
end
verify_command_line << version
verify_command_line << "local"
sh(*verify_command_line)
end
def apt_test_targets
targets = (ENV["APT_TARGETS"] || "").split(",")
targets = apt_test_targets_default if targets.empty?
targets
end
def apt_test_targets_default
# Disable arm64 targets by default for now
# because they require some setups on host.
[
"debian-bookworm",
# "debian-bookworm-arm64",
"debian-trixie",
# "debian-trixie-arm64",
"ubuntu-jammy",
# "ubuntu-jammy-arm64",
"ubuntu-noble",
# "ubuntu-noble-arm64",
]
end
def define_apt_test_task
namespace :apt do
desc "Test deb packages"
task :test do
repositories_dir = "apt/repositories"
unless @packages.empty?
rm_rf(repositories_dir)
@packages.each do |package|
package_repositories = "#{package}/apt/repositories"
next unless File.exist?(package_repositories)
sh("rsync", "-av", "#{package_repositories}/", repositories_dir)
end
end
Dir.glob("#{repositories_dir}/ubuntu/pool/*") do |code_name_dir|
universe_dir = "#{code_name_dir}/universe"
next unless File.exist?(universe_dir)
mv(universe_dir, "#{code_name_dir}/main")
end
base_dir = "nonexistent"
merged_dir = "apt/merged"
apt_update(base_dir, repositories_dir, merged_dir)
Dir.glob("#{merged_dir}/*/dists/*") do |dists_code_name_dir|
prefix = dists_code_name_dir.split("/")[-3..-1].join("/")
mv(Dir.glob("#{dists_code_name_dir}/*Release*"),
"#{repositories_dir}/#{prefix}")
end
apt_test_targets.each do |target|
verify(target)
end
end
end
end
def yum_test_targets
targets = (ENV["YUM_TARGETS"] || "").split(",")
targets = yum_test_targets_default if targets.empty?
targets
end
def yum_test_targets_default
# Disable aarch64 targets by default for now
# because they require some setups on host.
[
"almalinux-9",
# "almalinux-9-aarch64",
"almalinux-8",
# "almalinux-8-aarch64",
"amazon-linux-2023",
# "amazon-linux-2023-aarch64",
"centos-9-stream",
# "centos-9-stream-aarch64",
"centos-8-stream",
# "centos-8-stream-aarch64",
"centos-7",
# "centos-7-aarch64",
]
end
def define_yum_test_task
namespace :yum do
desc "Test RPM packages"
task :test do
repositories_dir = "yum/repositories"
unless @packages.empty?
rm_rf(repositories_dir)
@packages.each do |package|
package_repositories = "#{package}/yum/repositories"
next unless File.exist?(package_repositories)
sh("rsync", "-av", "#{package_repositories}/", repositories_dir)
end
end
rpm_sign(repositories_dir)
base_dir = "nonexistent"
yum_update(base_dir, repositories_dir)
yum_test_targets.each do |target|
verify(target)
end
end
end
end
end