You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
299 lines
8.1 KiB
Ruby
299 lines
8.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require 'open3'
|
|
require 'pathname'
|
|
require 'set'
|
|
|
|
module CI
|
|
extend self
|
|
|
|
def einfo str; $stderr.puts " \e[32;1m*\e[0m #{str}"; end
|
|
def ewarn str; $stderr.puts " \e[33;1m*\e[0m #{str}"; end
|
|
def eerror str; $stderr.puts " \e[31;1m*\e[0m #{str}"; end
|
|
def ecmd str; $stderr.puts " \e[35;1m*\e[0m #{str}"; end
|
|
|
|
# Replacement for Shellwords.shellescape. Unfortunately it outputs pretty
|
|
# unreadable shit as it escapes space with \ instead of putting it in quotes,
|
|
# and if there are like, newlines in the string, it puts just the newline into
|
|
# quotes. Our primary goal here is to have a *readable* output as the output
|
|
# normally won't be run through the shell, try to create something that's
|
|
# readable and also valid bash input.
|
|
refine String do
|
|
def shellescape
|
|
if self =~ %r{^[0-9a-zA-Z%+,./:=@^_'-]+$}
|
|
gsub "'", "\\\\'"
|
|
else
|
|
"'" + gsub("'", "'\\\\''") + "'"
|
|
end
|
|
end
|
|
|
|
# because adding this to ruby is fucking impossible, as there's an alternate
|
|
# solution, which is:
|
|
# base = expr()
|
|
# Dir.glob("*/glob...", base: base).map {|f| File.join base, f }
|
|
# instead of
|
|
# Dir.glob "#{Dir.glob_escape expr()}/*/glob..."
|
|
# or our version:
|
|
# Dir.glob "#{expr().globescape}/*/glob..."
|
|
# yeah, I'm totally gonna write that boilerplate every fucking time. And it
|
|
# also only works at the beginning of the path, not anywhere else. Why must
|
|
# every language became java?!
|
|
def globescape; gsub /\[|\]|\*|\?|\{|\}/, '\\\\' + '\0'; end
|
|
end
|
|
# Fuckin shellwords (required by nokogiri) puts shelljoin onto Array instead
|
|
# of Enumerable, so override it here too.
|
|
refine Array do
|
|
def shelljoin; map(&:shellescape).join ' '; end
|
|
end
|
|
refine Enumerable do
|
|
def shelljoin; map(&:shellescape).join ' '; end
|
|
end
|
|
using self
|
|
|
|
def get_path
|
|
opt.extra_path + ENV['PATH'].split(':')
|
|
end
|
|
|
|
def which cmd
|
|
get_path.lazy.map {|d| File.join(d, cmd) }.find {|f| File.executable? f }
|
|
end
|
|
|
|
class Store
|
|
include Enumerable
|
|
def initialize
|
|
@map = {}
|
|
end
|
|
|
|
def set name, val
|
|
name = name.to_sym
|
|
fail "Overwriting variable #{name}" if @map[name].is_a? Array
|
|
@map[name] = check_val val
|
|
end
|
|
|
|
def set! name, val; @map[name.to_sym] = check_val val; end
|
|
def unset name; @map.delete name.to_sym; end
|
|
def get name; @map[name.to_sym]; end
|
|
|
|
def method_missing name, *args
|
|
if name.end_with? '='
|
|
set name.to_s.chomp('='), *args
|
|
else
|
|
@map.[] name.to_sym, *args
|
|
end
|
|
end
|
|
|
|
def each &blk; @map.each &blk; end
|
|
|
|
private
|
|
def check_val val
|
|
unless val.is_a?(Array) || val.is_a?(Integer) || val.is_a?(String) ||
|
|
val == true || val == false
|
|
fail "Unsupported value type #{val.class}"
|
|
end
|
|
val
|
|
end
|
|
end
|
|
@env = Store.new; attr_reader :env
|
|
@opt = Store.new; attr_reader :opt
|
|
|
|
@last_env = {}
|
|
def calc_env args
|
|
args = args[0] if args.size == 1 && args[0].is_a?(Array)
|
|
now_env = env.map {|k,v| [k.to_s, v.is_a?(Array) ? v.shelljoin : v.to_s] }.to_h
|
|
now_env['PATH'] = get_path.join ':'
|
|
@last_env.each {|k,v| ecmd "unset #{k.shellescape}" unless now_env.key? k }
|
|
now_env.each do |k,v|
|
|
ecmd "export #{k.shellescape}=#{v.shellescape}" unless @last_env[k] == v
|
|
end
|
|
@last_env = now_env
|
|
args
|
|
end
|
|
def print_cd cd
|
|
ecmd "cd #{cd.shellescape}" unless @last_cd == cd
|
|
@last_cd = cd
|
|
end
|
|
private :print_cd
|
|
def maybe_run dir, *args
|
|
args = calc_env args
|
|
print_cd dir
|
|
ecmd args.shelljoin
|
|
system @last_env, *args, chdir: dir
|
|
end
|
|
|
|
def run dir, *args; maybe_run(dir, *args) or fail "Command failure: #{$?}"; end
|
|
|
|
def run_capture dir, *args
|
|
args = calc_env args
|
|
print_cd dir
|
|
ecmd args.shelljoin
|
|
out, st = Open3.capture2 @last_env, *args, chdir: dir
|
|
fail "Command failure: #{st}" unless st.success?
|
|
out
|
|
end
|
|
|
|
# random filesystem helpers that look like commands
|
|
def rm dir, *files
|
|
files = files[0] if files.size == 1 && files[0].is_a?(Array)
|
|
return if files.empty?
|
|
print_cd dir
|
|
ecmd "rm #{files.shelljoin}"
|
|
Dir.chdir(dir) { File.unlink *files }
|
|
end
|
|
|
|
def rm_if dir, *files
|
|
files = files[0] if files.size == 1 && files[0].is_a?(Array)
|
|
files.select! {|f| File.exist? File.join dir, f }
|
|
rm dir, *files
|
|
end
|
|
|
|
def rel_from path, from
|
|
res = Pathname.new(path).relative_path_from(from).to_s
|
|
# make sure it has at least a slash, in case it is used in shell, except .
|
|
# stays ., because ./. looks weird
|
|
res == ?. || res.include?('/') ? res : "./#{res}"
|
|
end
|
|
|
|
@anon_step_id = 0
|
|
@steps = {}
|
|
Step = Struct.new :blk, :dep, :always_dep, :cond
|
|
def get_step name
|
|
@steps[name] ||= Step.new nil, [], [], nil
|
|
end
|
|
private :get_step
|
|
|
|
def step name = nil, before: [], after: [], always_after: [], cond: nil, &blk
|
|
name = :"__anon_step_#{@anon_step_id += 1}" unless name
|
|
before = [before] unless before.is_a? Array
|
|
after = [after] unless after.is_a? Array
|
|
always_after = [always_after] unless always_after.is_a? Array
|
|
blk ||= -> {}
|
|
step = get_step name
|
|
if step.blk || step.cond
|
|
fail "Duplicate step #{name}. Previous: #{step.blk.source_location.join ':'}"
|
|
end
|
|
|
|
before.each {|s| get_step(s).dep << name }
|
|
after.each {|s| get_step s }
|
|
always_after.each {|s| get_step s }
|
|
step.blk = blk
|
|
step.dep.concat after
|
|
step.always_dep.concat always_after
|
|
step.cond = cond
|
|
true
|
|
end
|
|
|
|
def run_steps *steps
|
|
steps = steps[0] if steps.size == 1 && steps[0].is_a?(Array)
|
|
fail "No steps specified" if steps.empty?
|
|
|
|
state = {}
|
|
queue = steps.reverse
|
|
while !queue.empty?
|
|
step_id = queue.pop
|
|
step = @steps[step_id]
|
|
fail "Invalid step #{step_id}" unless step
|
|
|
|
case state[step_id]
|
|
when nil
|
|
queue << step_id
|
|
queue.concat step.always_dep.reverse
|
|
state[step_id] = :always
|
|
|
|
when :always
|
|
if step.cond && !step.cond[]
|
|
state[step_id] = :done
|
|
else
|
|
queue << step_id
|
|
queue.concat step.dep.reverse
|
|
state[step_id] = :deps
|
|
end
|
|
|
|
when :deps
|
|
if step.cond && !step.cond[]
|
|
ewarn "Step #{step_id} was scheduled, but condition changed, skipping"
|
|
else
|
|
step.blk[]
|
|
state[step_id] = :done
|
|
end
|
|
|
|
when :done
|
|
end
|
|
end
|
|
end
|
|
|
|
def check_steps
|
|
fail 'Step error' unless @steps.select {|k,s| s.blk.nil? }.each do |k,s|
|
|
eerror "Missing step dependency: #{k.inspect}"
|
|
end.empty?
|
|
end
|
|
|
|
HOUR = 60*60
|
|
MIN = 60
|
|
def format_time f
|
|
if f >= HOUR
|
|
sprintf "%d:%02d:%06.3f", f/HOUR, f/MIN%MIN, f%MIN
|
|
else
|
|
sprintf "%02d:%06.3f", f/MIN, f%MIN
|
|
end
|
|
end
|
|
|
|
# add test decorators around step
|
|
def test_step name, **args, &blk
|
|
step name, always_after: :final_opts, before: :test, **args do
|
|
einfo "Running test #{name}..."
|
|
start = Time.now
|
|
begin
|
|
blk[]
|
|
einfo "Test #{name} success (#{format_time Time.now - start}s)"
|
|
rescue
|
|
ewarn "Test #{name} failed (#{format_time Time.now - start}s): #{$!}"
|
|
puts $!.backtrace
|
|
mark_failed
|
|
end
|
|
end
|
|
end
|
|
|
|
@status = true; attr_reader :status
|
|
def mark_failed; @status = false; end
|
|
|
|
# Different function, to make `return` inside a `step` throw a
|
|
# `LocalJumpError` instead of silently terminating the script... See
|
|
# https://ruby-doc.org/core-3.0.2/Proc.html#class-Proc-label-Orphaned+Proc
|
|
# https://archive.vn/Q7LIl
|
|
def load_file name
|
|
instance_eval File.read(name), name
|
|
end
|
|
|
|
def main
|
|
[opt.tools_dir, *opt.extra_tools_dirs].each do |d|
|
|
Dir.glob 'ci_conf.*.rb', base: d do |fn|
|
|
load_file File.join(d, fn)
|
|
end
|
|
end
|
|
check_steps
|
|
|
|
steps = []
|
|
ARGV.each do |a|
|
|
if a =~ /^([^=]+)=(.*)/
|
|
opt.set $1, $2
|
|
else
|
|
steps << a.to_sym
|
|
end
|
|
end
|
|
run_steps *steps
|
|
|
|
status ? einfo('Success') : ewarn('Test failure')
|
|
exit status ? 0 : 42
|
|
rescue
|
|
eerror $!.to_s
|
|
puts $!.backtrace
|
|
exit 1
|
|
end
|
|
end
|
|
|
|
CI.opt.tools_dir = __dir__
|
|
CI.opt.extra_tools_dirs = []
|
|
CI.opt.libshit_dir = File.dirname __dir__
|
|
CI.opt.extra_path = []
|
|
CI.opt.run_dir = Dir.pwd
|