# 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 'fileutils' require 'uri' 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). class GCCConfiguration attr_reader :target def initialize(suffix, target, sysroot_sdk) @suffix = suffix @target = target @sysroot_sdk = sysroot_sdk end def cc "gcc#{@suffix}" end def cxx "g++#{@suffix}" end def configure @sysroot_sdk.nil? ? [] : ["--with-sysroot=#{@sysroot_sdk}"] end end def gcc_verbose(gcc_binary) read, write = IO.pipe sh({ 'LC_ALL' => 'C' }, gcc_binary, '--verbose', err: write) write.close output = read.read read.close output end def target_triple(cc_name) gcc_verbose(cc_name) .lines .find { |line| line.start_with? 'Target: ' } .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 pipe_to_command(command, chdir: target.to_path) do |stdin| stream_http(url) { |chunk| stdin.write chunk } end end # Makes a link from the source tree in the GCC tree. def link_frontend(source, destination) File.symlink Pathname.new(source).relative_path_from(destination), (destination + File.basename(source)) end # Replaces an elna-native directive in the line with its dg-* equivalent, # escaping the directive argument for dejagnu's Tcl evaluation. def convert_directive(line, directive, dg_name) line.sub(/\(\*\s*#{Regexp.escape directive}\s+(.*?)\s*\*\)/) do escaped = Regexp.last_match(1).gsub(/[\\\[\]$"]/) { |character| "\\#{character}" } "(* { #{dg_name} \"#{escaped}\" } *)" 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') output.puts convert_directive(line, '@Flags', 'dg-additional-options') end 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) } .sort "(* { dg-additional-sources \"#{sources * ' '}\" } *)" end 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? 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' convert_test main, test_target / 'sut.elna', category, extra_lines end namespace :gcc do 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. 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 end sh 'contrib/download_prerequisites', chdir: GCC_TREE.to_path end desc 'Link the frontend into the GCC source tree' task :link do 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] FileList['boot', 'include', 'COPYING3', 'README.md', 'gcc/gcc', 'gcc/*.in', 'gcc/lang*'].each do |file| link_frontend file, source_destination end FileList['gcc/dg.exp', 'README.md'].each do |file| link_frontend file, test_destination end destination = GCC_TREE / 'gcc/testsuite/lib' FileList['gcc/testlib/*'].each do |file| rm_f destination / File.basename(file) link_frontend file, destination end end desc 'Configure the bootstrap compiler' 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', '--disable-bootstrap', '--disable-multilib', '--enable-host-shared', '--with-system-zlib', "--target=#{build_target.target}", "--build=#{build_target.target}", "--host=#{build_target.target}", *build_target.configure ] env = { 'CC' => ENV['CC'] || build_target.cc, 'CXX' => ENV['CXX'] || build_target.cxx } env['CFLAGS'] = env['CXXFLAGS'] = '-O0 -g -fPIC -I/opt/homebrew/opt/flex/include' 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 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 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 rm_rf category_destination mkdir_p category_destination category_source.each_child do |test_path| test_target = category_destination + test_path.basename if test_path.directory? convert_multi_module_test test_path, test_target, category else convert_test test_path, test_target, category end end end end desc 'Run tests' task check: 'gcc:convert' do UI.step 'Running DejaGnu testsuite...' log_file = Pathname.new(GCC_HOST) / 'gcc/testsuite/elna/elna.log' 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' 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| GCC_HOST_INSTALL / 'include/c++' / GCC_VERSION / inc end sh 'clang-tidy', '-p', compile_db.to_path, '--extra-arg=-w', '--warnings-as-errors=*', *includes.collect { |inc| "--extra-arg=-cxx-isystem#{inc.realpath}" }, '--extra-arg=-stdlib=libstdc++', *sources end desc 'Build documentation' task :doc do sh 'make', 'html', chdir: File.join(GCC_HOST, 'gcc') end desc 'Run GCC linters and tests' task test: %w[gcc:tidy gcc:check] end desc 'Build the bootstrap compiler' task gcc: %w[gcc:download gcc:link gcc:configure gcc:make]