diff options
Diffstat (limited to 'rakelib')
| -rw-r--r-- | rakelib/compiler.rake | 7 | ||||
| -rw-r--r-- | rakelib/doc.rake | 1 | ||||
| -rw-r--r-- | rakelib/gcc.rake | 215 | ||||
| -rw-r--r-- | rakelib/ruby.rake | 11 | ||||
| -rw-r--r-- | rakelib/support/ui.rb | 25 | ||||
| -rw-r--r-- | rakelib/treesitter.rake | 9 |
6 files changed, 148 insertions, 120 deletions
diff --git a/rakelib/compiler.rake b/rakelib/compiler.rake index f443407..4731e1d 100644 --- a/rakelib/compiler.rake +++ b/rakelib/compiler.rake @@ -11,7 +11,7 @@ CLEAN.include 'build/gcc' Rake.application.add_loader('d', Rake::MakefileLoader.new) FileList['build/gcc/**/*.d'].each { |file| import file } -rule(/gcc\/.+\.o$/ => ->(file) { +rule(%r{gcc/.+\.o$} => lambda { |file| source = Pathname.new('source') + Pathname.new(file).relative_path_from('build/gcc').sub_ext('.elna') @@ -22,9 +22,8 @@ rule(/gcc\/.+\.o$/ => ->(file) { sh t.prerequisites.last, '-c', '-MD', '-MP', '-O0', '-g', '-I', 'source', '-o', t.name, t.source.to_s end -file 'build/gcc/elna' => FileList['source/**/*.elna'].reject { |file| - file != file.downcase -}.map { |file| +sources = FileList['source/**/*.elna'].select { |file| file == file.downcase } +file 'build/gcc/elna' => sources.map { |file| Pathname.new('build/gcc') + Pathname.new(file).relative_path_from('source').sub_ext('.o') } do |t| diff --git a/rakelib/doc.rake b/rakelib/doc.rake index 5f40107..bf92293 100644 --- a/rakelib/doc.rake +++ b/rakelib/doc.rake @@ -18,6 +18,7 @@ rule '.bbl' => '.bcf' do |t| end namespace :doc do + desc 'Compile LaTeX documentation' task tex: 'build/doc' do |t| sh 'pdflatex', '-output-directory', "../#{t.prerequisites.first}", 'report', chdir: 'doc' diff --git a/rakelib/gcc.rake b/rakelib/gcc.rake index 7ee9f5c..6dce877 100644 --- a/rakelib/gcc.rake +++ b/rakelib/gcc.rake @@ -5,8 +5,18 @@ require 'fileutils' require 'uri' -require 'net/http' -require 'open3' +require 'faraday' +require 'faraday/follow_redirects' +require 'English' +require_relative 'support/ui' + +# Dependencies. +GCC_VERSION = '16.2.0' +GCC_HOST = 'build/host/gcc' +GCC_HOST_INSTALL = Pathname.new 'build/host/install' +GCC_TREE = Pathname.new "build/tools/gcc-#{GCC_VERSION}" +GCC_PATCH = + "https://raw.githubusercontent.com/Homebrew/homebrew-core/refs/heads/main/Patches/gcc/gcc-#{GCC_VERSION}.diff".freeze # Use Homebrew GCC if available (it uses libstdc++, avoiding # macOS-specific libc++ incompatibilities in GCC's own sources). @@ -34,59 +44,50 @@ end def gcc_verbose(gcc_binary) read, write = IO.pipe - sh({'LC_ALL' => 'C'}, gcc_binary, '--verbose', err: write) + sh({ 'LC_ALL' => 'C' }, gcc_binary, '--verbose', err: write) write.close output = read.read read.close output end -def find_build_target(gcc_version) - if RUBY_PLATFORM =~ /darwin/ - suffix = '-' + gcc_version.split('.').first - mac_os_sdk = '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk' - else - suffix = '' - end - build_target = gcc_verbose(ENV.fetch 'CC', "gcc#{suffix}") +def target_triple(cc_name) + gcc_verbose(cc_name) .lines .find { |line| line.start_with? 'Target: ' } - .split(' ') + .split .last .strip +end + +def find_build_target(gcc_version) + is_darwin = RUBY_PLATFORM =~ /darwin/ + suffix = is_darwin ? "-#{gcc_version.split('.').first}" : '' + mac_os_sdk = '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk' if is_darwin + build_target = target_triple(ENV.fetch('CC', "gcc#{suffix}")) GCCConfiguration.new suffix, build_target, mac_os_sdk end +def pipe_to_command(command, chdir:, &) + IO.popen(command, 'w', chdir:, &) + UI.fatal "Command #{command.join(' ')} failed" unless $CHILD_STATUS.success? +end + +def stream_http(url, &) + conn = Faraday.new do |f| + f.response :follow_redirects + f.response :raise_error + end + conn.get(url) do |req| + req.options.on_data = ->(chunk, _, env) { yield chunk if (200..299).cover?(env.status) && !chunk.empty? } + end +end + def download_and_pipe(url, target, command) target.mkpath - - Net::HTTP.start(url.host, url.port, use_ssl: url.scheme == 'https') do |http| - request = Net::HTTP::Get.new url.request_uri - - http.request request do |response| - case response - when Net::HTTPRedirection - download_and_pipe URI.parse(response['location']), target, command - when Net::HTTPSuccess - Dir.chdir target.to_path do - Open3.popen2(*command) do |stdin, stdout, wait_thread| - Thread.new do - stdout.each { |line| puts line } - end - - response.read_body do |chunk| - stdin.write chunk - end - stdin.close - - wait_thread.value - end - end - else - response.error! - end - end + pipe_to_command(command, chdir: target.to_path) do |stdin| + stream_http(url) { |chunk| stdin.write chunk } end end @@ -104,60 +105,69 @@ def convert_directive(line, directive, dg_name) end end +def test_trailer_lines(category, extra_lines = []) + lines = [] + lines << '(* { dg-do run } *)' if category == 'runnable' + lines.concat extra_lines + lines << '(* { dg-prune-output .* } *)' + case category + when 'compilable' then lines << '(* { dg-final { output-exists } } *)' + when 'fail_compilation' then lines << '(* { dg-final { output-exists-not } } *)' + end + lines +end + def convert_test(source, target, category, extra_lines = []) File.open(target, 'wb') do |output| File.foreach(source) do |line| line = convert_directive(line, '@Error', 'dg-error') - line = convert_directive(line, '@Flags', 'dg-additional-options') - - output.puts line + output.puts convert_directive(line, '@Flags', 'dg-additional-options') end - # Runnable tests compile, link and execute. - output.puts '(* { dg-do run } *)' if category == 'runnable' + test_trailer_lines(category, extra_lines).each { |line| output.puts line } + end +end + +def multi_module_extra_sources(test_target) + sources = test_target.glob('**/*.elna') + .map { |extra| %("#{extra.relative_path_from(test_target)}") } + "(* { dg-additional-sources #{sources * ' '} } *)" +end - extra_lines.each { |line| output.puts line } +def convert_multi_module_test(test_path, test_target, category) + main = test_path / 'sut.elna' + UI.fatal "Multi-module test directory \"#{test_path}\" must contain sut.elna." unless main.file? - # Suppress excess output so stray diagnostics do not fail the test. - output.puts '(* { dg-prune-output .* } *)' + test_target.mkdir + (test_path.children - [main]).each { |extra| cp_r extra, test_target } + extra_lines = ["(* { dg-additional-options \"-I#{test_target.expand_path}\" } *)"] + extra_lines << multi_module_extra_sources(test_target) if category == 'runnable' - case category - when 'compilable' - # Assert that an output file was produced. - output.puts '(* { dg-final { output-exists } } *)' - when 'fail_compilation' - # Assert that no output file was produced. - output.puts '(* { dg-final { output-exists-not } } *)' - end - end + convert_test main, test_target / 'sut.elna', category, extra_lines end namespace :gcc do - # Dependencies. - GCC_VERSION = "16.2.0" - HOST_GCC = 'build/host/gcc' - HOST_INSTALL = Pathname.new 'build/host/install' - GCC_TREE = Pathname.new "build/tools/gcc-#{GCC_VERSION}" - GCC_PATCH = - "https://raw.githubusercontent.com/Homebrew/homebrew-core/refs/heads/main/Patches/gcc/gcc-#{GCC_VERSION}.diff" - - directory HOST_GCC - directory HOST_INSTALL.to_path + directory GCC_HOST + directory GCC_HOST_INSTALL.to_path directory 'build/tools' desc 'Download the bootstrap compiler and its prerequisites' task download: 'build/tools' do + UI.step "Downloading bootstrap compiler and prerequisites (GCC #{GCC_VERSION})..." url = URI.parse "https://gcc.gnu.org/pub/gcc/releases/gcc-#{GCC_VERSION}/gcc-#{GCC_VERSION}.tar.xz" + UI.debug "Downloading #{url}..." download_and_pipe url, GCC_TREE.dirname, ['tar', '-Jxv'] + UI.debug "Downloading and applying patch #{GCC_PATCH}..." download_and_pipe URI.parse(GCC_PATCH), GCC_TREE, ['patch', '-p1'] # GCC 16.1.0 registers 17 languages but CL_PARAMS is at bit 16, # which collides with the 17th language class (CL_Rust). Shift # every CL_* constant from bit 16 upward by one position. - File.open(GCC_TREE + 'gcc/opts.h', 'r+') do |opts_h| - content = opts_h.read.gsub(/\(1U << (\d+)\)/) do |m| - "(1U << #{$1.to_i + 1})" + UI.debug 'Patching gcc/opts.h to shift CL_* flags...' + File.open(GCC_TREE / 'gcc/opts.h', 'r+') do |opts_h| + content = opts_h.read.gsub(/\(1U << (\d+)\)/) do + "(1U << #{Regexp.last_match(1).to_i + 1})" end opts_h.seek 0, IO::SEEK_SET opts_h.write content @@ -168,8 +178,9 @@ namespace :gcc do desc 'Link the frontend into the GCC source tree' task :link do - source_destination = GCC_TREE + 'gcc/elna' - test_destination = GCC_TREE + 'gcc/testsuite/elna.dg' + UI.step 'Linking Elna frontend into the GCC source tree...' + source_destination = GCC_TREE / 'gcc/elna' + test_destination = GCC_TREE / 'gcc/testsuite/elna.dg' rm_rf [source_destination, test_destination] mkdir_p [source_destination, test_destination] @@ -180,16 +191,17 @@ namespace :gcc do FileList['gcc/dg.exp', 'README.md'].each do |file| link_frontend file, test_destination end - destination = GCC_TREE + 'gcc/testsuite/lib' + destination = GCC_TREE / 'gcc/testsuite/lib' FileList['gcc/testlib/*'].each do |file| - rm_f (destination + File.basename(file)) + rm_f destination / File.basename(file) link_frontend file, destination end end desc 'Configure the bootstrap compiler' - task configure: [HOST_GCC, HOST_INSTALL.to_path] do |t| + task configure: [GCC_HOST, GCC_HOST_INSTALL.to_path] do |t| build_target = find_build_target GCC_VERSION + UI.step "Configuring bootstrap compiler for #{build_target.target}..." configure_options = [ "--prefix=#{File.realpath t.prerequisites.last}", '--enable-languages=c,c++,jit,elna', @@ -208,25 +220,28 @@ namespace :gcc do } env['CFLAGS'] = env['CXXFLAGS'] = '-O0 -g -fPIC -I/opt/homebrew/opt/flex/include' - configure = GCC_TREE.relative_path_from(HOST_GCC) + 'configure' - sh env, configure.to_path, *configure_options, chdir: HOST_GCC + configure = GCC_TREE.relative_path_from(GCC_HOST) / 'configure' + sh env, configure.to_path, *configure_options, chdir: GCC_HOST end desc 'Make and install the bootstrap compiler' task :make do - sh 'make', '-j', Etc.nprocessors.to_s, chdir: HOST_GCC - sh 'make', 'install', chdir: HOST_GCC + UI.step "Building bootstrap compiler (jobs: #{Etc.nprocessors})..." + sh 'make', '-j', Etc.nprocessors.to_s, chdir: GCC_HOST + sh 'make', 'install', chdir: GCC_HOST end desc 'Convert the testsuite into dejagnu format' task :convert do - destination = GCC_TREE + 'gcc/testsuite/elna.dg' + UI.step 'Converting testsuite to DejaGnu format...' + destination = GCC_TREE / 'gcc/testsuite/elna.dg' destination.mkpath source = Pathname.new 'testsuite' # Regenerate the category directories from scratch: this removes # converted tests whose sources are gone and adds new ones. %w[compilable fail_compilation runnable].each do |category| + UI.debug "Converting #{category} tests..." category_destination = destination + category category_source = source + category @@ -235,32 +250,8 @@ namespace :gcc do category_source.each_child do |test_path| test_target = category_destination + test_path.basename - - # Multi-module tests: directories containing a sut.elna (system under - # test) and any helper files, compiled and linked together. if test_path.directory? - main = test_path + 'sut.elna' - raise %(multi-module test directory \"#{test_path}\" must contain sut.elna) unless main - - test_target.mkdir - modules = test_path.children - [main] - - modules.each do |extra| - cp_r extra, test_target - end - extra_lines = ["(* { dg-additional-options \"-I#{test_target.expand_path}\" } *)"] - - # Only 'runnable' tests link the helper modules into an executable, - # so only they need the helper sources compiled as separate - # translation units via dg-additional-sources. Elna resolves imports - # on its own, so passing it as an additional source there would add a - # second positional. - if category == 'runnable' - extra_sources = test_target.glob("**/*.elna") - .map { |extra| %("#{extra.relative_path_from test_target}") } - extra_lines << "(* { dg-additional-sources #{extra_sources * ' '} } *)" - end - convert_test main, (test_target + 'sut.elna'), category, extra_lines + convert_multi_module_test test_path, test_target, category else convert_test test_path, test_target, category end @@ -270,25 +261,27 @@ namespace :gcc do desc 'Run tests' task check: 'gcc:convert' do - log_file = Pathname.new(HOST_GCC) + 'gcc/testsuite/elna/elna.log' - - sh 'make', 'check-elna', chdir: File.join(HOST_GCC, 'gcc') + UI.step 'Running DejaGnu testsuite...' + log_file = Pathname.new(GCC_HOST) / 'gcc/testsuite/elna/elna.log' - fail "\nSee #{log_file}." if log_file.file? and log_file.read =~ /^(FAIL|XPASS|UNRESOLVED|ERROR):/ + sh 'make', 'check-elna', chdir: File.join(GCC_HOST, 'gcc') + UI.fatal "See #{log_file}." if log_file.file? && log_file.read =~ /^(FAIL|XPASS|UNRESOLVED|ERROR):/ end desc 'Run clang-tidy' task :tidy do compile_db = Pathname.new 'compile_commands.json' - raise "#{compile_db} is missing. Run: bear -- rake gcc:make" unless compile_db.exist? + UI.fatal "#{compile_db} is missing.", hint: 'Run: bear -- rake gcc:make' unless compile_db.exist? + sources = FileList['boot/*.cc', 'gcc/gcc/*.cc', 'include/elna/**/*.h'] .reject do |file| ['/elna1.', '/elna-spec.'].any? { |pattern| file.include? pattern } end + UI.step "Running clang-tidy on #{sources.size} files..." build_target = find_build_target GCC_VERSION includes = ['.', build_target.target].collect do |inc| - HOST_INSTALL + 'include/c++' + GCC_VERSION + inc + GCC_HOST_INSTALL / 'include/c++' / GCC_VERSION / inc end sh 'clang-tidy', '-p', compile_db.to_path, '--extra-arg=-w', '--warnings-as-errors=*', @@ -298,7 +291,7 @@ namespace :gcc do desc 'Build documentation' task :doc do - sh 'make', 'html', chdir: File.join(HOST_GCC, 'gcc') + sh 'make', 'html', chdir: File.join(GCC_HOST, 'gcc') end desc 'Run GCC linters and tests' diff --git a/rakelib/ruby.rake b/rakelib/ruby.rake new file mode 100644 index 0000000..c3db30a --- /dev/null +++ b/rakelib/ruby.rake @@ -0,0 +1,11 @@ +# This Source Code Form is subject to the terms of the Mozilla Public License, +# v. 2.0. If a copy of the MPL was not distributed with this file, You can +# obtain one at https://mozilla.org/MPL/2.0/. +# frozen_string_literal: true + +require 'rubocop/rake_task' + +namespace :ruby do + desc 'Run RuboCop' + RuboCop::RakeTask.new :lint +end diff --git a/rakelib/support/ui.rb b/rakelib/support/ui.rb new file mode 100644 index 0000000..3120721 --- /dev/null +++ b/rakelib/support/ui.rb @@ -0,0 +1,25 @@ +# This Source Code Form is subject to the terms of the Mozilla Public License, +# v. 2.0. If a copy of the MPL was not distributed with this file, You can +# obtain one at https://mozilla.org/MPL/2.0/. +# frozen_string_literal: true + +require 'term/ansicolor' + +# Terminal UI helpers for build milestones, debug logging, and operational errors. +module UI + extend Term::ANSIColor + + def self.step(message) + puts green(message) + end + + def self.debug(message) + puts "#{dark(' ->')} #{dark(message)}" if Rake.verbose + end + + def self.fatal(message, hint: nil) + output = red(message) + output += "\n#{bold(cyan('Hint:'))} #{hint}" if hint + abort output + end +end diff --git a/rakelib/treesitter.rake b/rakelib/treesitter.rake index 7e48865..ca0c8d0 100644 --- a/rakelib/treesitter.rake +++ b/rakelib/treesitter.rake @@ -18,8 +18,8 @@ namespace :ts do cp_r TREE_SITTER_TREE, TREE_SITTER_BUILD unless TREE_SITTER_BUILD.directory? sh 'tree-sitter', 'generate', - '--output', (TREE_SITTER_BUILD + 'src').to_path, - (TREE_SITTER_TREE + 'grammar.js').to_path + '--output', (TREE_SITTER_BUILD / 'src').to_path, + (TREE_SITTER_TREE / 'grammar.js').to_path build_test = TREE_SITTER_BUILD / 'test' build_corpus = build_test / 'corpus' @@ -44,7 +44,7 @@ namespace :ts do File.open target, 'w' do |output| output.write "==================\n" - output.write(File.basename(test, '.elna') + "\n") + output.write "#{File.basename(test, '.elna')}\n" output.write "==================\n\n" output.write File.read(test) output.write "\n---\n\n" @@ -61,5 +61,4 @@ namespace :ts do end desc 'Build a tree-sitter parser' -task ts: 'ts:generate' -task ts: "#{TREE_SITTER_BUILD}/parser.so" +task ts: ['ts:generate', "#{TREE_SITTER_BUILD}/parser.so"] |
