From 83623567704fbc9f15e363a492cd0cff9bbd8a5c Mon Sep 17 00:00:00 2001 From: Joe Eli McIlvain Date: Thu, 5 May 2022 10:19:25 -0700 Subject: [PATCH] Add support for (cross-compiling to) Windows. Note that because Crystal doesn't fully support Windows yet, the Savi compiler still cannot run on native Windows. But Savi programs can, after being cross-compiled. After this change, it is possible to build 64-bit Windows binaries from a Linux host running the Savi compiler, after setting the `SDK_ROOT` environment variable to point to a directory tree where the Windows SDK libraries can be found (such as an MSVC installation), using a command like this: ```savi savi build --cross-compile=x86_64-unknown-windows-msvc ``` Note that the Savi compiler includes the `lld` linker inside it, so it is not necessary to have access to the full MSVC toolchain - only the `.lib` files must be reachable within the `SDK_ROOT` tree. One convenient way to automate the downloading of these libraries on non-Windows platforms is [`xwin`](https://github.com/Jake-Shadle/xwin). For example, running the following commands should be sufficient (after reviewing the Windows SDK license and agreeing to its terms): ```savi xwin --accept-license 1 splat --output /tmp/xwin env SDK_ROOT=/tmp/xwin savi build --cross-compile=x86_64-unknown-windows-msvc ``` Note that not all of the Savi standard library works on Windows yet - getting this new compiler with cross-compilation support for Windows will allow the Savi team to iterate on bringing each library into full Windows support and adding Windows CI to all repos. --- Makefile | 2 +- main.cr | 4 +++ src/savi/compiler.cr | 1 + src/savi/compiler/binary.cr | 54 +++++++++++++++++++++++++++--- src/savi/compiler/binary_object.cr | 4 +++ src/savi/compiler/code_gen.cr | 13 ++++--- src/savi/compiler/context.cr | 6 ++-- 7 files changed, 72 insertions(+), 12 deletions(-) diff --git a/Makefile b/Makefile index 9171beb9..bd96404d 100644 --- a/Makefile +++ b/Makefile @@ -137,7 +137,7 @@ $(eval $(call MAKE_VAR_CACHE_FOR,LLVM_STATIC_RELEASE_URL)) # Specify where to download our pre-built runtime bitcode from. # This needs to get bumped explicitly here when we do a new runtime build. -RUNTIME_BITCODE_RELEASE_URL?=https://github.com/savi-lang/runtime-bitcode/releases/download/20220206 +RUNTIME_BITCODE_RELEASE_URL?=https://github.com/savi-lang/runtime-bitcode/releases/download/v0.20220505.0 $(eval $(call MAKE_VAR_CACHE_FOR,RUNTIME_BITCODE_RELEASE_URL)) # This is the path where we look for the LLVM pre-built static libraries to be, diff --git a/main.cr b/main.cr index 377a6299..522abe6d 100644 --- a/main.cr +++ b/main.cr @@ -21,6 +21,7 @@ module Savi option "--llvm-ir", desc: "Write generated LLVM IR to a file", type: Bool, default: false option "--llvm-keep-fns", desc: "Don't allow LLVM to remove functions from the output", type: Bool, default: false option "--print-perf", desc: "Print compiler performance info", type: Bool, default: false + option "-X", "--cross-compile=TRIPLE", desc: "Cross compile to the given target triple" option "-C", "--cd=DIR", desc: "Change the working directory" option "-p NAME", "--pass=NAME", desc: "Name of the compiler pass to target" run do |opts, args| @@ -33,6 +34,7 @@ module Savi options.llvm_keep_fns = true if opts.llvm_keep_fns options.auto_fix = true if opts.fix options.target_pass = Savi::Compiler.pass_symbol(opts.pass) if opts.pass + options.cross_compile = opts.cross_compile.not_nil! if opts.cross_compile Dir.cd(opts.cd.not_nil!) if opts.cd Cli.compile options, opts.backtrace end @@ -111,6 +113,7 @@ module Savi option "--llvm-ir", desc: "Write generated LLVM IR to a file", type: Bool, default: false option "--llvm-keep-fns", desc: "Don't allow LLVM to remove functions from the output", type: Bool, default: false option "--print-perf", desc: "Print compiler performance info", type: Bool, default: false + option "-X", "--cross-compile=TRIPLE", desc: "Cross compile to the given target triple" option "-C", "--cd=DIR", desc: "Change the working directory" run do |opts, args| options = Savi::Compiler::Options.new( @@ -122,6 +125,7 @@ module Savi options.llvm_keep_fns = true if opts.llvm_keep_fns options.auto_fix = true if opts.fix options.manifest_name = args.name.not_nil! if args.name + options.cross_compile = opts.cross_compile.not_nil! if opts.cross_compile Dir.cd(opts.cd.not_nil!) if opts.cd Cli.compile options, opts.backtrace end diff --git a/src/savi/compiler.cr b/src/savi/compiler.cr index f0416bb7..8032b14f 100644 --- a/src/savi/compiler.cr +++ b/src/savi/compiler.cr @@ -11,6 +11,7 @@ class Savi::Compiler property llvm_ir = false property llvm_keep_fns = false property auto_fix = false + property cross_compile : String? = nil property manifest_name : String? property target_pass : Symbol? diff --git a/src/savi/compiler/binary.cr b/src/savi/compiler/binary.cr index 05ac0baf..2e2a72b5 100644 --- a/src/savi/compiler/binary.cr +++ b/src/savi/compiler/binary.cr @@ -24,6 +24,7 @@ class Savi::Compiler::Binary def run(ctx) target = Target.new(ctx.code_gen.target_machine.triple) bin_path = Binary.path_for(ctx) + bin_path += ".exe" if target.windows? # Compile a temporary binary object file, that we will remove after we # use it in the linker invocation to create the final binary. @@ -36,6 +37,8 @@ class Savi::Compiler::Binary link_for_linux_or_bsd(ctx, target, obj_path, bin_path) elsif target.macos? link_for_macosx(ctx, target, obj_path, bin_path) + elsif target.windows? + link_for_windows(ctx, target, obj_path, bin_path) else raise NotImplementedError.new(target.inspect) end @@ -75,7 +78,9 @@ class Savi::Compiler::Binary # Set up the main library paths. # TODO: Support overriding (supplementing?) this via the `SDK_ROOT` env var. - each_sysroot_lib_path(target) { |lib_path| link_args << "-L#{lib_path}" } + each_sysroot_lib_path(ctx, target) { |lib_path| + link_args << "-L#{lib_path}" + } # Link the main system libraries. link_args << "-lSystem" @@ -94,6 +99,29 @@ class Savi::Compiler::Binary invoke_linker("mach_o", link_args) end + # Link a EXE executable for a Windows target. + def link_for_windows(ctx, target, obj_path, bin_path) + link_args = %w{lld-link -nologo -defaultlib:libcmt -defaultlib:oldnames} + + # Set up the main library paths. + each_sysroot_lib_path(ctx, target) { |lib_path| + link_args << "-libpath:#{lib_path}" + } + + # Specify the base set of libraries to link to. + link_args << "-defaultlib:libcmt" # always needed + link_args << "-defaultlib:oldnames" # always needed + link_args << "-defaultlib:dbghelp" # used by runtime platform/ponyassert.c + link_args << "-defaultlib:ws2_32" # used by runtime lang/socket.c + + # Finally, specify the input object file and the output filename. + link_args << obj_path + link_args << "-out:#{bin_path}" + + # Invoke the linker, using the COFF flavor. + invoke_linker("coff", link_args) + end + # Link an ELF executable for a Linux or FreeBSD target. def link_for_linux_or_bsd(ctx, target, obj_path, bin_path) link_args = %w{ld.lld} @@ -118,7 +146,7 @@ class Savi::Compiler::Binary # Get the list of lib search paths within the sysroot. lib_paths = [] of String - each_sysroot_lib_path(target) { |path| lib_paths << path } + each_sysroot_lib_path(ctx, target) { |path| lib_paths << path } lib_paths.each { |lib_path| link_args << "-L#{lib_path}" } # Also find the mandatory ceremony objects that all programs need to link. @@ -173,11 +201,11 @@ class Savi::Compiler::Binary end # Yield each sysroot-based path in which to search for linkable libs/objs. - def each_sysroot_lib_path(target) + def each_sysroot_lib_path(ctx, target) sysroot = "/" # TODO: Allow user to supply custom sysroot for cross-compile. yielded_any = false - each_sysroot_lib_glob(target) { |lib_glob| + each_sysroot_lib_glob(ctx, target) { |lib_glob| Dir.glob(lib_glob) { |lib_path| next unless Dir.exists?(lib_path) @@ -190,13 +218,29 @@ class Savi::Compiler::Binary end # Yield each sysroot-based glob used to find paths that exist. - def each_sysroot_lib_glob(target) + def each_sysroot_lib_glob(ctx, target) # For MacOS, we have just one valid sysroot path, so we can finish early. if target.macos? yield "/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib" return end + # For Windows we only allow cross compiling and require this env var. + if target.windows? + unless ctx.options.cross_compile + raise NotImplementedError.new("Windows is only supported by cross-compiling.") + end + + sdk_root = ENV["SDK_ROOT"]? + unless sdk_root + raise NotImplementedError.new("The env var `SDK_ROOT` is required to cross-compile to Windows.") + end + + yield "#{sdk_root}/**/x86_64" + yield "#{sdk_root}/**/x64" + return + end + if target.linux? if target.musl? if target.x86_64? diff --git a/src/savi/compiler/binary_object.cr b/src/savi/compiler/binary_object.cr index efb9424a..c400334b 100644 --- a/src/savi/compiler/binary_object.cr +++ b/src/savi/compiler/binary_object.cr @@ -55,6 +55,10 @@ class Savi::Compiler::BinaryObject elsif target.arm64? return "arm64-apple-macosx" end + elsif target.windows? + if target.x86_64? && target.msvc? + return "x86_64-unknown-windows-msvc" + end end raise NotImplementedError.new(target.inspect) diff --git a/src/savi/compiler/code_gen.cr b/src/savi/compiler/code_gen.cr index 59545c91..32009cd0 100644 --- a/src/savi/compiler/code_gen.cr +++ b/src/savi/compiler/code_gen.cr @@ -92,11 +92,16 @@ class Savi::Compiler::CodeGen getter bitwidth getter isize - def initialize(runtime : PonyRT.class | VeronaRT.class = PonyRT) + def initialize( + runtime : PonyRT.class | VeronaRT.class, + options : Compiler::Options + ) LLVM.init_x86 LLVM.init_aarch64 LLVM.init_arm - @target_triple = LLVM.configured_default_target_triple.as(String) + @target_triple = ( + options.cross_compile || LLVM.configured_default_target_triple + ).as(String) @target = LLVM::Target.from_triple(@target_triple) @target_machine = @target.create_target_machine(@target_triple).as(LLVM::TargetMachine) @llvm = LLVM::Context.new @@ -1021,9 +1026,9 @@ class Savi::Compiler::CodeGen when "is_macos" gen_bool(target.macos?) when "is_posix" - gen_bool(true) # TODO: false on windows + gen_bool(!target.windows?) when "is_windows" - gen_bool(false) # TODO: true on windows + gen_bool(target.windows?) when "is_ilp32" gen_bool(abi_size_of(@isize) == 4) when "is_lp64" diff --git a/src/savi/compiler/context.cr b/src/savi/compiler/context.cr index 327d8abf..806a2627 100644 --- a/src/savi/compiler/context.cr +++ b/src/savi/compiler/context.cr @@ -4,8 +4,8 @@ class Savi::Compiler::Context getter program = Program.new getter classify = Classify::Pass.new - getter code_gen = CodeGen.new(CodeGen::PonyRT) - getter code_gen_verona = CodeGen.new(CodeGen::VeronaRT) + getter code_gen : CodeGen + getter code_gen_verona : CodeGen getter completeness = Completeness::Pass.new getter run = Run.new getter flow = Flow::Pass.new @@ -54,6 +54,8 @@ class Savi::Compiler::Context getter errors = [] of Error def initialize(@compiler, @options = Compiler::Options.new, @prev_ctx = nil) + @code_gen = CodeGen.new(CodeGen::PonyRT, @options) + @code_gen_verona = CodeGen.new(CodeGen::VeronaRT, @options) end def root_package