Skip to content

Commit

Permalink
Automatically detect MSVC tools on Windows interpreter (#14391)
Browse files Browse the repository at this point in the history
  • Loading branch information
HertzDevil committed Mar 25, 2024
1 parent 905039c commit f0afa88
Show file tree
Hide file tree
Showing 3 changed files with 54 additions and 30 deletions.
42 changes: 42 additions & 0 deletions src/compiler/crystal/codegen/link.cr
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
{% if flag?(:msvc) %}
require "crystal/system/win32/visual_studio"
require "crystal/system/win32/windows_sdk"
{% end %}

module Crystal
struct LinkAnnotation
getter lib : String?
Expand Down Expand Up @@ -272,6 +277,43 @@ module Crystal
end
end

# Detects the current MSVC linker and the relevant linker flags that
# recreate the MSVC developer prompt's standard library paths. If both MSVC
# and the Windows SDK are available, the linker will be an absolute path and
# the linker flags will contain the `/LIBPATH`s for the system libraries.
#
# Has no effect if the host compiler is not using MSVC.
def msvc_compiler_and_flags : {String, Array(String)}
linker = Compiler::MSVC_LINKER
link_args = [] of String

{% if flag?(:msvc) %}
if msvc_path = Crystal::System::VisualStudio.find_latest_msvc_path
if win_sdk_libpath = Crystal::System::WindowsSDK.find_win10_sdk_libpath
host_bits = {{ flag?(:aarch64) ? "ARM64" : flag?(:bits64) ? "x64" : "x86" }}
target_bits = has_flag?("aarch64") ? "arm64" : has_flag?("bits64") ? "x64" : "x86"

# MSVC build tools and Windows SDK found; recreate `LIB` environment variable
# that is normally expected on the MSVC developer command prompt
link_args << "/LIBPATH:#{msvc_path.join("atlmfc", "lib", target_bits)}"
link_args << "/LIBPATH:#{msvc_path.join("lib", target_bits)}"
link_args << "/LIBPATH:#{win_sdk_libpath.join("ucrt", target_bits)}"
link_args << "/LIBPATH:#{win_sdk_libpath.join("um", target_bits)}"

# use exact path for compiler instead of relying on `PATH`, unless
# explicitly overridden by `%CC%`
# (letter case shouldn't matter in most cases but being exact doesn't hurt here)
unless ENV.has_key?("CC")
target_bits = target_bits.sub("arm", "ARM")
linker = msvc_path.join("bin", "Host#{host_bits}", target_bits, "cl.exe").to_s
end
end
end
{% end %}

{linker, link_args}
end

PKG_CONFIG_PATH = Process.find_executable("pkg-config")

# Returns the result of running `pkg-config mod` but returns nil if
Expand Down
35 changes: 5 additions & 30 deletions src/compiler/crystal/compiler.cr
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ require "colorize"
require "crystal/digest/md5"
{% if flag?(:msvc) %}
require "./loader"
require "crystal/system/win32/visual_studio"
require "crystal/system/win32/windows_sdk"
{% end %}

module Crystal
Expand All @@ -27,8 +25,8 @@ module Crystal
# A Compiler parses source code, type checks it and
# optionally generates an executable.
class Compiler
private DEFAULT_LINKER = ENV["CC"]? || {{ env("CRYSTAL_CONFIG_CC") || "cc" }}
private MSVC_LINKER = ENV["CC"]? || {{ env("CRYSTAL_CONFIG_CC") || "cl.exe" }}
DEFAULT_LINKER = ENV["CC"]? || {{ env("CRYSTAL_CONFIG_CC") || "cc" }}
MSVC_LINKER = ENV["CC"]? || {{ env("CRYSTAL_CONFIG_CC") || "cl.exe" }}

# A source to the compiler: its filename and source code.
record Source,
Expand Down Expand Up @@ -409,32 +407,9 @@ module Crystal
object_arg = Process.quote_windows(object_names)
output_arg = Process.quote_windows("/Fe#{output_filename}")

linker = MSVC_LINKER
link_args = [] of String

# if the compiler and the target both have the `msvc` flag, we are not
# cross-compiling and therefore we should attempt detecting MSVC's
# standard paths
{% if flag?(:msvc) %}
if msvc_path = Crystal::System::VisualStudio.find_latest_msvc_path
if win_sdk_libpath = Crystal::System::WindowsSDK.find_win10_sdk_libpath
host_bits = {{ flag?(:aarch64) ? "ARM64" : flag?(:bits64) ? "x64" : "x86" }}
target_bits = program.has_flag?("aarch64") ? "arm64" : program.has_flag?("bits64") ? "x64" : "x86"

# MSVC build tools and Windows SDK found; recreate `LIB` environment variable
# that is normally expected on the MSVC developer command prompt
link_args << Process.quote_windows("/LIBPATH:#{msvc_path.join("atlmfc", "lib", target_bits)}")
link_args << Process.quote_windows("/LIBPATH:#{msvc_path.join("lib", target_bits)}")
link_args << Process.quote_windows("/LIBPATH:#{win_sdk_libpath.join("ucrt", target_bits)}")
link_args << Process.quote_windows("/LIBPATH:#{win_sdk_libpath.join("um", target_bits)}")

# use exact path for compiler instead of relying on `PATH`
# (letter case shouldn't matter in most cases but being exact doesn't hurt here)
target_bits = target_bits.sub("arm", "ARM")
linker = Process.quote_windows(msvc_path.join("bin", "Host#{host_bits}", target_bits, "cl.exe").to_s) unless ENV.has_key?("CC")
end
end
{% end %}
linker, link_args = program.msvc_compiler_and_flags
linker = Process.quote_windows(linker)
link_args.map! { |arg| Process.quote_windows(arg) }

link_args << "/DEBUG:FULL /PDBALTPATH:%_PDB%" unless debug.none?
link_args << "/INCREMENTAL:NO /STACK:0x800000"
Expand Down
7 changes: 7 additions & 0 deletions src/compiler/crystal/interpreter/context.cr
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,13 @@ class Crystal::Repl::Context
# (MSVC doesn't seem to have this issue)
args.delete("-lgc")

# recreate the MSVC developer prompt environment, similar to how compiled
# code does it in `Compiler#linker_command`
if program.has_flag?("msvc")
_, link_args = program.msvc_compiler_and_flags
args.concat(link_args)
end

Crystal::Loader.parse(args, dll_search_paths: dll_search_paths).tap do |loader|
# FIXME: Part 2: This is a workaround for initial integration of the interpreter:
# We append a handle to the current executable (i.e. the compiler program)
Expand Down

0 comments on commit f0afa88

Please sign in to comment.