#!/usr/bin/env ruby # 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 'uri' require 'net/http' require 'open3' require 'etc' require 'pathname' require 'rake/clean' TMP = Pathname.new('./build') CLOBBER.include 'build' def gcc_verbose(gcc_binary) read, write = IO.pipe system({'LANG' => 'C'}, gcc_binary, '--verbose', err: write) write.close output = read.read read.close output end class BuildTarget def initialize(architecture) case architecture when /^r(isc)?v32$/ @architecture = :rv32 else raise "Unsupported architecture '#{architecture}'." end end def sysroot TMP + 'sysroot' end def rootfs TMP + 'rootfs' end def target case @architecture when :rv32 'riscv32-unknown-linux-gnu' end end def build @build ||= gcc_verbose(ENV['CC']).lines .find { |line| line.start_with? 'Target: ' } .split(' ').last.strip end def configuration(prefix) case @architecture when :rv32 ['arch=rv32imafdc', 'abi=ilp32d', 'tune=rocket', 'isa-spec=20191213'].map do |option| "-#{prefix}#{option}" end else [] end end end def download_and_unarchive(url) case File.extname url.path when '.bz2' archive_type = '-j' root_directory = File.basename url.path, '.tar.bz2' when '.xz' archive_type = '-J' root_directory = File.basename url.path, '.tar.xz' else raise "Unsupported archive type #{url.path}." end target = TMP + 'tools' 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_unarchive URI.parse(response['location']) when Net::HTTPSuccess Open3.popen2 'tar', '-C', target.to_path, archive_type, '-xv' 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 else response.error! end end end target + root_directory end def configure_make_install(source_directory, configure_options, env, cwd) configure = source_directory.relative_path_from(cwd) + 'configure' sh env, configure.to_path, *configure_options, chdir: cwd.to_path sh 'make', '-j', Etc.nprocessors.to_s, chdir: cwd.to_path sh env, 'make', 'install', chdir: cwd.to_path end directory(TMP + 'tools') namespace :cross do BINUTILS_VERSION = '2.44' GLIBC_VERSION = '2.41' KERNEL_VERSION = '5.15.185' GCC_VERSION = "15.1.0" task :environment, :target do |t, args| binary_suffix = '' output = gcc_verbose 'gcc' if output.start_with? 'Apple clang' binary_suffix = "-#{GCC_VERSION.split('.').first}" end ENV['CC'] = "gcc#{binary_suffix}" ENV['CXX'] = "g++#{binary_suffix}" end desc 'Build cross binutils' task :binutils, [:target] => :environment do |t, args| options = BuildTarget.new args[:target] source_directory = download_and_unarchive( URI.parse("https://ftp.gnu.org/gnu/binutils/binutils-#{BINUTILS_VERSION}.tar.xz")) cwd = source_directory.dirname + 'build-binutils' cwd.mkpath options.rootfs.mkpath env = ENV.slice 'CC', 'CXX' configure_options = [ "--prefix=#{options.rootfs.realpath}", "--target=#{options.target}", '--disable-nls', '--enable-gprofng=no', '--disable-werror', '--enable-default-hash-style=gnu', '--disable-libquadmath' ] configure_make_install source_directory, configure_options, env, cwd end desc 'Build stage 1 GCC' task :gcc1, [:target] => :environment do |t, args| options = BuildTarget.new args[:target] source_directory = download_and_unarchive( URI.parse("https://gcc.gnu.org/pub/gcc/releases/gcc-#{GCC_VERSION}/gcc-#{GCC_VERSION}.tar.xz")) cwd = source_directory.dirname + 'build-gcc' cwd.mkpath options.rootfs.mkpath options.sysroot.mkpath sh 'contrib/download_prerequisites', chdir: source_directory.to_path configure_options = options.configuration('-with-') + [ "--prefix=#{options.rootfs.realpath}", "--with-sysroot=#{options.sysroot.realpath}", '--enable-languages=c,c++', '--disable-shared', '--disable-bootstrap', '--disable-multilib', '--disable-libmudflap', '--disable-libssp', '--disable-libquadmath', '--disable-libsanitizer', '--disable-threads', '--disable-libatomic', '--disable-libgomp', '--disable-libvtv', '--disable-libstdcxx', '--disable-nls', '--with-newlib', '--without-headers', "--target=#{options.target}", "--build=#{options.build}", "--host=#{options.build}" ] flags = '-O2 -fPIC' env = { 'CC' => ENV['CC'], 'CXX' => ENV['CXX'], 'CFLAGS' => flags, 'CXXFLAGS' => flags, 'PATH' => "#{options.rootfs.realpath + 'bin'}:#{ENV['PATH']}" } configure_make_install source_directory, configure_options, env, cwd end desc 'Copy glibc headers' task :headers, [:target] => :environment do |t, args| options = BuildTarget.new args[:target] source_directory = download_and_unarchive( URI.parse("https://ftp.gnu.org/gnu/glibc/glibc-#{GLIBC_VERSION}.tar.xz")) include_directory = TMP + 'tools/include' include_directory.mkpath cp (source_directory + 'elf/elf.h'), (include_directory + 'elf.h') end desc 'Build linux kernel' task :kernel, [:target] => :environment do |t, args| options = BuildTarget.new args[:target] cwd = download_and_unarchive( URI.parse("https://cdn.kernel.org/pub/linux/kernel/v5.x/linux-#{KERNEL_VERSION}.tar.xz")) env = { 'CROSS_COMPILE' => "#{options.target}-", 'ARCH' => 'riscv', 'PATH' => "#{options.rootfs.realpath + 'bin'}:#{ENV['PATH']}", 'HOSTCFLAGS' => "-D_UUID_T -D__GETHOSTUUID_H -I#{TMP + 'tools/include'}" } sh env, 'make', 'rv32_defconfig', chdir: cwd.to_path sh env, 'make', '-j', Etc.nprocessors.to_s, chdir: cwd.to_path sh env, 'make', 'headers', chdir: cwd.to_path user_directory = options.sysroot + 'usr' user_directory.mkpath cp_r (cwd + 'usr/include'), (user_directory + 'include') end desc 'Build glibc' task :glibc, [:target] => :environment do |t, args| options = BuildTarget.new args[:target] source_directory = TMP + "tools/glibc-#{GLIBC_VERSION}" configure_options = [ '--prefix=/usr', "--host=#{options.target}", "--target=#{options.target}", "--build=#{options.build}", "--enable-kernel=#{KERNEL_VERSION}", "--with-headers=#{options.sysroot.realpath + 'usr/include'}", '--disable-nscd', '--disable-libquadmath', '--disable-libitm', '--disable-werror', 'libc_cv_forced_unwind=yes' ] bin = options.rootfs.realpath + 'bin' env = { 'PATH' => "#{bin}:#{ENV['PATH']}", 'MAKE' => 'make', # Otherwise it uses gnumake which can be different and too old. 'CC' => "#{options.target}-gcc", 'CXX' => "#{options.target}-g++" } cwd = source_directory.dirname + 'build-glibc' cwd.mkpath configure = source_directory.relative_path_from(cwd) +'./configure' system env, configure.to_path, *configure_options, chdir: cwd.to_path, exception: true system env, 'make', '-j', Etc.nprocessors.to_s, chdir: cwd.to_path, exception: true system env, 'make', "install_root=#{options.sysroot.realpath}", 'install', chdir: cwd.to_path, exception: true end desc 'Build stage 2 GCC' task :gcc2, [:target] => :environment do |t, args| options = BuildTarget.new args[:target] source_directory = TMP + "tools/gcc-#{GCC_VERSION}" cwd = TMP + 'tools/build-gcc' rm_rf cwd cwd.mkpath cp_r '../elna', source_directory + 'gcc' configure_options = options.configuration('-with-') + [ "--prefix=#{options.rootfs.realpath}", "--with-sysroot=#{options.sysroot.realpath}", '--enable-languages=c,c++,lto,elna', '--enable-lto', '--enable-shared', '--disable-bootstrap', '--disable-multilib', '--enable-checking=release', '--disable-libssp', '--disable-libquadmath', '--enable-threads=posix', '--with-default-libstdcxx-abi=new', '--disable-nls', "--target=#{options.target}", "--build=#{options.build}", "--host=#{options.build}" ] flags = '-O2 -fPIC' env = { 'CFLAGS' => flags, 'CXXFLAGS' => flags, 'PATH' => "#{options.rootfs.realpath + 'bin'}:#{ENV['PATH']}" } configure_make_install source_directory, configure_options, env, cwd end end task cross: TMP + 'tools' task :cross, :target do |t, args| args.with_defaults target: 'riscv32' Rake::Task['cross:binutils'].invoke args[:target] Rake::Task['cross:gcc1'].invoke args[:target] Rake::Task['cross:headers'].invoke args[:target] Rake::Task['cross:kernel'].invoke args[:target] Rake::Task['cross:glibc'].invoke args[:target] Rake::Task['cross:gcc2'].invoke args[:target] end def relative_from_tmp(path) Pathname.new(path).relative_path_from(TMP).each_filename end CLEAN.include(TMP + 'riscv32') directory(TMP + 'riscv32') rule '.s.o' => ->(match) { architecture, *path_components = relative_from_tmp(match).to_a path_components[-1] = path_components.last.ext('') [File.join(path_components), TMP + architecture] } do |t| options = BuildTarget.new relative_from_tmp(t.name).first compiler = options.rootfs + "bin/#{options.target}-gcc" flags = ['-ffreestanding'] + options.configuration('m') sources = t.prerequisites.select { |prerequisite| File.extname(prerequisite) == '.s' } system compiler.to_s, *flags, '-c', '-o', t.name, *sources, exception: true end rule '.elna.o' => ->(match) { architecture, *path_components = relative_from_tmp(match).to_a path_components[-1] = path_components.last.ext('') [File.join(path_components), TMP + architecture] } do |t| options = BuildTarget.new relative_from_tmp(t.name).first compiler = options.rootfs + "bin/#{options.target}-gcc" flags = ['-O2', '-g3', '-fno-stack-protector'] + options.configuration('m') sources = t.prerequisites.select { |prerequisite| File.extname(prerequisite) == '.elna' } system compiler.to_s, *flags, '-c', '-o', t.name, *sources, exception: true end rule 'kernel.elf' => ->(match) { architecture_tmp = TMP + relative_from_tmp(match).first FileList['*.s', '*.elna'].map { |source| (architecture_tmp + source).to_path + '.o' } << 'kernel.ld' } do |t| options = BuildTarget.new relative_from_tmp(t.name).first compiler = options.rootfs + "bin/#{options.target}-gcc" flags = options.configuration('m') objects, linker_script = t.prerequisites.partition { |prerequisite| File.extname(prerequisite) == '.o' } system compiler.to_s, '-nostdlib', '-T', *linker_script, '-o', t.name, *objects, exception: true end task default: 'build/riscv32/kernel.elf' task :default do |t| QEMU = 'qemu-system-riscv32' system QEMU, '-machine', 'virt', '-bios', 'default', '-nographic', '-serial', 'mon:stdio', '--no-reboot', '-kernel', *t.prerequisites end