aboutsummaryrefslogtreecommitdiff
path: root/rakelib/gcc.rake
blob: 6dce877d427a7600084e48cfc24aa25def449e83 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# 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)}") }
  "(* { 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]