metacc 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3f277277e6c19c84a200cf69d21452da422980b3955c895bc148666b2e170d7c
4
- data.tar.gz: e17a0627af2e670202be743c4e745a6aedaf10284ead45cd7c6740dd64dcf4b0
3
+ metadata.gz: a14fc8ca9c502275a4cc8105906722c167fb58cf6a269559748da22cf88f83d3
4
+ data.tar.gz: f56a289ba4240caf683f8614f5317252753032ad8902cce314a0ab43380d05fd
5
5
  SHA512:
6
- metadata.gz: d889eff550e1f704c4e4264d750c1d03ddd7e040c1303eb6147501de5b0b2eec970c595cf183afe6fe7da91ee9465c0aa70f7596887018d3b9f4e979801e9f27
7
- data.tar.gz: 9a0ae1d9c54d117d809dc236396ee0d4a69b321161a535287195474cdd6845a78dc0352bfe0b51f80487dbe25d9194a2afb98320ed2c90ff458f857d2be92f53
6
+ metadata.gz: 2b5c6055a1445b3e0341fbde3da0ca9b76e2cfe021c0c7694be91233a2eb2c68de7f063cc5c2b9b953b697fdfa41f3a949b168eb33790d8544fedeeef90f222a
7
+ data.tar.gz: 30b826250c85ce5d7b57c83f81714d7ac51b6e911abed4ea8371e3bc36540f0b29779aeced94b398b7d590ffbbbf608c7ab2929f0d9743a8967824edad8bea59
data/bin/metacc CHANGED
@@ -5,4 +5,10 @@ $LOAD_PATH.unshift(File.join(__dir__, "..", "lib"))
5
5
 
6
6
  require "metacc/cli"
7
7
 
8
- MetaCC::CLI.new.run(ARGV)
8
+ begin
9
+ MetaCC::CLI.new.run(ARGV)
10
+ rescue MetaCC::CLI::InvalidOption, MetaCC::ToolchainNotFoundError => e
11
+ warn "#{$0} error: #{e.message}"
12
+ rescue MetaCC::CompileError => e
13
+ warn e.message
14
+ end
data/lib/metacc/cli.rb CHANGED
@@ -16,7 +16,7 @@ module MetaCC
16
16
  # --std=c++11 --std=c++14 --std=c++17 --std=c++20 --std=c++23 --std=c++26
17
17
  #
18
18
  # Linking:
19
- # --objects / -c – compile only; don't link
19
+ # -c – compile only; don't link
20
20
  # -l, -L - specify linker input
21
21
  # --shared – produce a shared library
22
22
  # --static – produce a static library
@@ -69,9 +69,9 @@ module MetaCC
69
69
  }.freeze
70
70
 
71
71
  STANDARDS = {
72
- "c11" => :c11,
73
- "c17" => :c17,
74
- "c23" => :c23,
72
+ "c11" => :c11,
73
+ "c17" => :c17,
74
+ "c23" => :c23,
75
75
  "c++11" => :cxx11,
76
76
  "c++14" => :cxx14,
77
77
  "c++17" => :cxx17,
@@ -82,42 +82,46 @@ module MetaCC
82
82
 
83
83
  # Maps --x<name> CLI option names to xflags toolchain-class keys.
84
84
  XFLAGS = {
85
- "xmsvc" => MSVC,
86
- "xgnu" => GNU,
87
- "xclang" => Clang,
85
+ "xmsvc" => MSVC,
86
+ "xgnu" => GNU,
87
+ "xclang" => Clang,
88
88
  "xclangcl" => ClangCL
89
89
  }.freeze
90
90
 
91
- def run(argv, driver: Driver.new)
92
- options, input_paths = parse_compile_args(argv, driver:)
91
+ def initialize(driver: Driver.new)
92
+ @driver = driver
93
+ end
94
+
95
+ def run(argv)
96
+ input_paths, options = parse_compile_args(argv)
93
97
  output_path = options.delete(:output_path)
94
- run_flag = options.delete(:run)
95
- validate_options!(options[:flags], output_path, run_flag)
96
- invoke(driver, input_paths, output_path, options, run: run_flag)
98
+ validate_options!(options[:flags], output_path, link: options[:link], run: options[:run])
99
+ invoke(input_paths, output_path, **options)
97
100
  end
98
101
 
99
102
  # Parses compile arguments.
100
- # Returns [options_hash, remaining_positional_args].
101
- def parse_compile_args(argv, driver: nil)
103
+ # Returns [positional_args, options_hash].
104
+ def parse_compile_args(argv)
102
105
  options = {
103
106
  include_paths: [],
104
- defs: [],
105
- linker_paths: [],
106
- libs: [],
107
- output_path: nil,
108
- run: false,
109
- flags: [],
110
- xflags: {},
107
+ defs: [],
108
+ link: true,
109
+ link_paths: [],
110
+ libs: [],
111
+ output_path: nil,
112
+ run: false,
113
+ flags: [],
114
+ xflags: {}
111
115
  }
112
116
  parser = OptionParser.new
113
- setup_compile_options(parser, options, driver:)
114
- sources = parser.permute(argv)
115
- [options, sources]
117
+ setup_compile_options(parser, options)
118
+ input_paths = parser.permute(argv)
119
+ [input_paths, options]
116
120
  end
117
121
 
118
122
  private
119
123
 
120
- def setup_compile_options(parser, options, driver: nil)
124
+ def setup_compile_options(parser, options)
121
125
  parser.on("-o FILEPATH", "Output file path") do |value|
122
126
  options[:output_path] = value
123
127
  end
@@ -127,26 +131,23 @@ module MetaCC
127
131
  parser.on("-D DEF", "Add a preprocessor definition") do |value|
128
132
  options[:defs] << value
129
133
  end
130
- parser.on("-O LEVEL", /\A[0-3]\z/, "Optimization level (0–3)") do |level|
131
- options[:flags] << :"o#{l}"
132
- end
133
- parser.on("-Os", "Optimize for size") do
134
- options[:flags] << :os
134
+ parser.on("-O LEVEL", /\A[0-3]|s\z/, "Optimization level (0–3)") do |level|
135
+ options[:flags] << :"o#{level}"
135
136
  end
136
137
  parser.on("-m", "--arch ARCH", "Target architecture") do |value|
137
- options[:flags] << TARGETS[v]
138
+ options[:flags] << TARGETS[value]
138
139
  end
139
140
  parser.on("-g", "--debug", "Emit debugging symbols") do
140
141
  options[:flags] << :debug
141
142
  end
142
143
  parser.on("--std STANDARD", "Specify the language standard") do |value|
143
- options[:flags] << STANDARDS[v]
144
+ options[:flags] << STANDARDS[value]
144
145
  end
145
146
  parser.on("-W OPTION", "Configure warnings") do |value|
146
- options[:flags] << WARNING_CONFIGS[v]
147
+ options[:flags] << WARNING_CONFIGS[value]
147
148
  end
148
- parser.on("-c", "--objects", "Produce object files") do
149
- options[:flags] << :objects
149
+ parser.on("-c", "Produce object files") do
150
+ options[:link] = false
150
151
  end
151
152
  parser.on("-r", "--run", "Run the compiled executable after a successful build") do
152
153
  options[:run] = true
@@ -155,7 +156,7 @@ module MetaCC
155
156
  options[:libs] << value
156
157
  end
157
158
  parser.on("-L DIR", "Add linker library search path") do |value|
158
- options[:linker_paths] << value
159
+ options[:link_paths] << value
159
160
  end
160
161
  parser.on("--shared", "Produce a shared library") do
161
162
  options[:flags] << :shared
@@ -178,39 +179,37 @@ module MetaCC
178
179
  end
179
180
  end
180
181
  parser.on_tail("--version", "Print the toolchain version and exit") do
181
- puts driver&.toolchain&.version_banner
182
+ puts @driver.toolchain.version_banner
182
183
  exit
183
184
  end
184
185
  end
185
186
 
186
- def validate_options!(flags, output_path, run_flag)
187
- objects = flags.include?(:objects)
188
-
189
- if objects && output_path
190
- warn "error: -o cannot be used with --objects"
191
- exit 1
187
+ def validate_options!(flags, output_path, link:, run:)
188
+ if !link && output_path
189
+ raise InvalidOption, "cannot specify output path (-o) in compile only mode (-c)"
192
190
  end
193
191
 
194
- unless objects || output_path
195
- warn "error: -o is required"
196
- exit 1
192
+ if link && !output_path
193
+ raise InvalidOption, "must specify an output path (-o)"
197
194
  end
198
195
 
199
- if run_flag && (objects || flags.include?(:shared) || flags.include?(:static))
200
- warn "error: --run cannot be used with --objects, --shared, or --static"
201
- exit 1
196
+ if run && (!link || flags.include?(:shared) || flags.include?(:static))
197
+ raise InvalidOption, "--run may not be used with -c, --shared, or --static"
202
198
  end
203
199
  end
204
200
 
205
- def run_executable(path)
206
- system(path)
201
+ def invoke(input_paths, desired_output_path = nil, link: true, run: false, **options)
202
+ if link
203
+ actual_output_path = @driver.compile_and_link(input_paths, desired_output_path, **options)
204
+ system(actual_output_path) if run
205
+ else
206
+ options.delete(:link_paths)
207
+ options.delete(:libs)
208
+ @driver.compile(input_paths, **options)
209
+ end
207
210
  end
208
211
 
209
- def invoke(driver, input_paths, output_path, options, run: false)
210
- result = driver.invoke(input_paths, output_path, **options)
211
- exit 1 unless result
212
- run_executable(result) if run
213
- end
212
+ class InvalidOption < StandardError; end
214
213
 
215
214
  end
216
215
 
data/lib/metacc/driver.rb CHANGED
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "open3"
3
4
  require_relative "toolchain"
4
5
 
5
6
  module MetaCC
6
7
 
7
8
  # Raised when no supported C/C++ compiler can be found on the system.
8
- class CompilerNotFoundError < StandardError; end
9
+ class CompileError < StandardError; end
10
+
11
+ # Raised when no supported C/C++ compiler can be found on the system.
12
+ class ToolchainNotFoundError < StandardError; end
9
13
 
10
14
  # Driver wraps C and C++ compile and link operations using the first
11
15
  # available compiler found on the system (Clang, GCC, or MSVC).
@@ -22,7 +26,7 @@ module MetaCC
22
26
  asan ubsan msan
23
27
  no_rtti no_exceptions pic
24
28
  no_semantic_interposition no_omit_frame_pointer no_strict_aliasing
25
- objects shared static strip
29
+ shared static strip
26
30
  ]
27
31
  ).freeze
28
32
 
@@ -36,16 +40,48 @@ module MetaCC
36
40
  # Defaults to [Clang, GNU, MSVC].
37
41
  # @param search_paths [Array<String>] directories to search for toolchain executables
38
42
  # before falling back to PATH. Defaults to [].
39
- # @raise [CompilerNotFoundError] if no supported compiler is found.
40
- def initialize(prefer: [Clang, GNU, MSVC],
41
- search_paths: [])
43
+ # @raise [ToolchainNotFoundError] if no supported compiler is found.
44
+ def initialize(prefer: [MSVC], search_paths: [])
42
45
  @toolchain = select_toolchain!(prefer, search_paths)
43
46
  end
44
47
 
48
+ # Invokes the compiler driver for the given input files and output path,
49
+ # compiling and producting object files without linking.
50
+ #
51
+ # @param input_files [String, Array<String>] paths to the input files
52
+ # @param flags [Array<Symbol>] compiler/linker flags
53
+ # @param xflags [Hash{Class => String}] extra (native) compiler flags keyed by toolchain Class
54
+ # @param include_paths [Array<String>] directories to add with -I
55
+ # @param defs [Array<String>] preprocessor macros (e.g. "FOO" or "FOO=1")
56
+ # @param env [Hash] environment variables to set for the subprocess
57
+ # @param working_dir [String] working directory for the subprocess (default: ".")
58
+ # @raise [CompileError] if the underlying toolchain executable returns a non-zero exit status
59
+ def compile(
60
+ input_files,
61
+ flags: [],
62
+ xflags: {},
63
+ include_paths: [],
64
+ defs: [],
65
+ env: {},
66
+ working_dir: "."
67
+ )
68
+ flags = translate_flags(flags)
69
+ flags.concat(xflags[@toolchain.class] || [])
70
+
71
+ cmd = @toolchain.compile_command(
72
+ input_files,
73
+ flags:,
74
+ include_paths:,
75
+ defs:
76
+ )
77
+
78
+ !!run_command(cmd, env:, working_dir:)
79
+ end
80
+
45
81
  # Invokes the compiler driver for the given input files and output path.
46
82
  # The kind of output (object files, executable, shared library, or static
47
- # library) is determined by the flags: +:objects+, +:shared+, or +:static+.
48
- # When none of these mode flags is present, an executable is produced.
83
+ # library) is determined by the flags: +:shared+ or +:static+. When none of
84
+ # these mode flags is present, an executable is produced.
49
85
  #
50
86
  # @param input_files [String, Array<String>] paths to the input files
51
87
  # @param output_path [String] path for the resulting output file
@@ -53,37 +89,45 @@ module MetaCC
53
89
  # @param xflags [Hash{Class => String}] extra (native) compiler flags keyed by toolchain Class
54
90
  # @param include_paths [Array<String>] directories to add with -I
55
91
  # @param defs [Array<String>] preprocessor macros (e.g. "FOO" or "FOO=1")
56
- # @param libs [Array<String>] library names to link (e.g. "m", "pthread")
57
92
  # @param linker_paths [Array<String>] linker library search paths (-L / /LIBPATH:)
93
+ # @param libs [Array<String>] library names to link (e.g. "m", "pthread")
58
94
  # @param env [Hash] environment variables to set for the subprocess
59
95
  # @param working_dir [String] working directory for the subprocess (default: ".")
60
- # @return [String, true, nil] the (possibly extension-augmented) output path on success,
61
- # true if output_path was nil and the command succeeded,
62
- # nil if the underlying toolchain executable returned a non-zero exit status
63
- # @raise [ArgumentError] if output_path is nil and the :objects flag is not present
64
- def invoke(
96
+ # @return [String] the (possibly extension-augmented) output path on success
97
+ # @raise [CompileError] if the underlying toolchain executable returns a non-zero exit status
98
+ def compile_and_link(
65
99
  input_files,
66
100
  output_path,
67
101
  flags: [],
68
102
  xflags: {},
69
103
  include_paths: [],
70
104
  defs: [],
105
+ link_paths: [],
71
106
  libs: [],
72
- linker_paths: [],
73
107
  env: {},
74
108
  working_dir: "."
75
109
  )
76
- output_type = output_type_from_flags(flags)
77
- raise ArgumentError, "output_path must not be nil" if output_path.nil? && output_type != :objects
78
-
79
- output_path = apply_default_extension(output_path, output_type) unless output_path.nil?
110
+ output_type = if flags.include?(:shared) then :shared
111
+ elsif flags.include?(:static) then :static
112
+ else :executable
113
+ end
114
+ output_path = apply_default_extension(output_path, output_type)
80
115
 
81
- input_files = Array(input_files)
82
116
  flags = translate_flags(flags)
83
117
  flags.concat(xflags[@toolchain.class] || [])
84
118
 
85
- cmd = @toolchain.command(input_files, output_path, flags, include_paths, defs, libs, linker_paths)
86
- run_command(cmd, env:, working_dir:) ? (output_path || true) : nil
119
+ cmds = @toolchain.compile_and_link_commands(
120
+ input_files,
121
+ output_path,
122
+ flags:,
123
+ include_paths:,
124
+ defs:,
125
+ libs:,
126
+ link_paths:
127
+ )
128
+
129
+ cmds.each { |cmd| run_command(cmd, env:, working_dir:) }
130
+ output_path
87
131
  end
88
132
 
89
133
  private
@@ -93,15 +137,8 @@ module MetaCC
93
137
  toolchain = toolchain_class.new(search_paths:)
94
138
  return toolchain if toolchain.available?
95
139
  end
96
- raise CompilerNotFoundError, "No supported C/C++ compiler found (tried clang, gcc, cl)"
97
- end
98
-
99
- def output_type_from_flags(flags)
100
- if flags.include?(:objects) then :objects
101
- elsif flags.include?(:shared) then :shared
102
- elsif flags.include?(:static) then :static
103
- else :executable
104
- end
140
+ candidate_names = candidates.map { |candidate| candidate.name.split("::").last }
141
+ raise ToolchainNotFoundError, "no supported C/C++ toolchain found (tried #{candidate_names.join(", ")})"
105
142
  end
106
143
 
107
144
  def apply_default_extension(path, output_type)
@@ -121,7 +158,8 @@ module MetaCC
121
158
  end
122
159
 
123
160
  def run_command(cmd, env: {}, working_dir: ".")
124
- !!system(env, *cmd, chdir: working_dir, out: File::NULL, err: File::NULL)
161
+ _out, err, status = Open3.capture3(env, *cmd, chdir: working_dir)
162
+ raise CompileError, err unless status.success?
125
163
  end
126
164
 
127
165
  end
@@ -17,7 +17,7 @@ module MetaCC
17
17
  @search_paths = search_paths
18
18
  end
19
19
 
20
- # Returns true if this toolchain's primary compiler is present in PATH.
20
+ # Returns true if this toolchain's primary compiler can be found
21
21
  def available?
22
22
  command_available?(c)
23
23
  end
@@ -26,14 +26,12 @@ module MetaCC
26
26
  # The default implementation returns [:c, :cxx]. Subclasses that only
27
27
  # support a subset of languages should override this method.
28
28
  def languages
29
- [:c, :cxx]
29
+ %i[c cxx]
30
30
  end
31
31
 
32
32
  # Returns true if +command+ is present in PATH, false otherwise.
33
33
  # Intentionally ignores the exit status – only ENOENT (not found) matters.
34
34
  def command_available?(command)
35
- return false if command.nil?
36
-
37
35
  !system(command, "--version", out: File::NULL, err: File::NULL).nil?
38
36
  end
39
37
 
@@ -44,14 +42,31 @@ module MetaCC
44
42
 
45
43
  # Returns a Hash mapping universal flags to native flags for this toolchain.
46
44
  def flags
47
- raise RuntimeError, "#{self.class}#flags not implemented"
45
+ raise "#{self.class}#flags not implemented"
48
46
  end
49
47
 
50
48
  # Returns the full command array for the given inputs, output, and flags.
51
49
  # The output mode (object files, shared library, static library, or
52
50
  # executable) is determined by the translated flags.
53
- def command(input_files, output, flags, include_paths, definitions, libs, linker_include_dirs)
54
- raise RuntimeError, "#{self.class}#command not implemented"
51
+ def compile_command(
52
+ input_files,
53
+ flags:,
54
+ include_paths:,
55
+ defs:
56
+ )
57
+ raise "#{self.class}#command not implemented"
58
+ end
59
+
60
+ def compile_and_link_commands(
61
+ input_files,
62
+ output_file,
63
+ flags:,
64
+ include_paths:,
65
+ defs:,
66
+ link_paths:,
67
+ libs:
68
+ )
69
+ raise "#{self.class}#command not implemented"
55
70
  end
56
71
 
57
72
  # Returns the default file extension (with leading dot, e.g. ".o") for the
@@ -102,57 +117,73 @@ module MetaCC
102
117
  # GNU-compatible toolchain (gcc).
103
118
  class GNU < Toolchain
104
119
 
105
- def initialize(search_paths: [])
106
- super
107
- @c = resolve_command("gcc")
120
+ def initialize(cc_command = "gcc", search_paths: [])
121
+ super(search_paths:)
122
+ @c = resolve_command(cc_command)
108
123
  end
109
124
 
110
- def command(input_files, output, flags, include_paths, definitions, libs, linker_include_dirs)
125
+ def compile_command(
126
+ input_files,
127
+ flags:,
128
+ include_paths:,
129
+ defs:
130
+ )
111
131
  inc_flags = include_paths.map { |p| "-I#{p}" }
112
- def_flags = definitions.map { |d| "-D#{d}" }
113
- link_mode = !flags.include?("-c")
114
- lib_path_flags = link_mode ? linker_include_dirs.map { |p| "-L#{p}" } : []
115
- lib_flags = link_mode ? libs.map { |l| "-l#{l}" } : []
116
- cmd = [c, *flags, *inc_flags, *def_flags, *input_files, *lib_path_flags, *lib_flags]
117
- output.nil? ? cmd : [*cmd, "-o", output]
132
+ def_flags = defs.map { |d| "-D#{d}" }
133
+ [c, "-c", *flags, *inc_flags, *def_flags, *input_files]
134
+ end
135
+
136
+ def compile_and_link_commands(
137
+ input_files,
138
+ output_file,
139
+ flags:,
140
+ include_paths:,
141
+ defs:,
142
+ link_paths:,
143
+ libs:
144
+ )
145
+ inc_flags = include_paths.map { |p| "-I#{p}" }
146
+ def_flags = defs.map { |d| "-D#{d}" }
147
+ lib_path_flags = link_paths.map { |p| "-L#{p}" }
148
+ lib_flags = libs.map { |l| "-l#{l}" }
149
+ [[c, *flags, *inc_flags, *def_flags, *input_files, *lib_path_flags, *lib_flags, "-o", output_file]]
118
150
  end
119
151
 
120
152
  GNU_FLAGS = {
121
- o0: ["-O0"],
122
- o1: ["-O1"],
123
- o2: ["-O2"],
124
- o3: ["-O3"],
125
- os: ["-Os"],
126
- sse4_2: ["-march=x86-64-v2"], # This is a better match for /arch:SSE4.2 than -msse4_2 is
127
- avx: ["-march=x86-64-v2", "-mavx"],
128
- avx2: ["-march=x86-64-v3"], # This is a better match for /arch:AVX2 than -mavx2 is
129
- avx512: ["-march=x86-64-v4"],
130
- native: ["-march=native", "-mtune=native"],
131
- debug: ["-g3"],
132
- lto: ["-flto"],
133
- warn_all: ["-Wall", "-Wextra", "-pedantic"],
134
- warn_error: ["-Werror"],
135
- c11: ["-std=c11"],
136
- c17: ["-std=c17"],
137
- c23: ["-std=c23"],
138
- cxx11: ["-std=c++11"],
139
- cxx14: ["-std=c++14"],
140
- cxx17: ["-std=c++17"],
141
- cxx20: ["-std=c++20"],
142
- cxx23: ["-std=c++23"],
143
- cxx26: ["-std=c++2c"],
144
- asan: ["-fsanitize=address"],
145
- ubsan: ["-fsanitize=undefined"],
146
- msan: ["-fsanitize=memory"],
153
+ o0: ["-O0"],
154
+ o1: ["-O1"],
155
+ o2: ["-O2"],
156
+ o3: ["-O3"],
157
+ os: ["-Os"],
158
+ sse4_2: ["-march=x86-64-v2"], # This is a better match for /arch:SSE4.2 than -msse4_2 is
159
+ avx: ["-march=x86-64-v2", "-mavx"],
160
+ avx2: ["-march=x86-64-v3"], # This is a better match for /arch:AVX2 than -mavx2 is
161
+ avx512: ["-march=x86-64-v4"],
162
+ native: ["-march=native", "-mtune=native"],
163
+ debug: ["-g3"],
164
+ lto: ["-flto"],
165
+ warn_all: ["-Wall", "-Wextra", "-pedantic"],
166
+ warn_error: ["-Werror"],
167
+ c11: ["-std=c11"],
168
+ c17: ["-std=c17"],
169
+ c23: ["-std=c23"],
170
+ cxx11: ["-std=c++11"],
171
+ cxx14: ["-std=c++14"],
172
+ cxx17: ["-std=c++17"],
173
+ cxx20: ["-std=c++20"],
174
+ cxx23: ["-std=c++23"],
175
+ cxx26: ["-std=c++2c"],
176
+ asan: ["-fsanitize=address"],
177
+ ubsan: ["-fsanitize=undefined"],
178
+ msan: ["-fsanitize=memory"],
147
179
  no_rtti: ["-fno-rtti"],
148
180
  no_exceptions: ["-fno-exceptions", "-fno-unwind-tables"],
149
181
  pic: ["-fPIC"],
150
182
  no_semantic_interposition: ["-fno-semantic-interposition"],
151
183
  no_omit_frame_pointer: ["-fno-omit-frame-pointer"],
152
184
  no_strict_aliasing: ["-fno-strict-aliasing"],
153
- objects: ["-c"],
154
185
  shared: ["-shared"],
155
- static: ["-r", "-nostdlib"],
186
+ static: ["-static"],
156
187
  strip: ["-Wl,--strip-unneeded"]
157
188
  }.freeze
158
189
 
@@ -166,8 +197,7 @@ module MetaCC
166
197
  class Clang < GNU
167
198
 
168
199
  def initialize(search_paths: [])
169
- super
170
- @c = resolve_command("clang")
200
+ super("clang", search_paths:)
171
201
  end
172
202
 
173
203
  CLANG_FLAGS = GNU_FLAGS.merge(lto: ["-flto=thin"]).freeze
@@ -190,24 +220,37 @@ module MetaCC
190
220
  def initialize(cl_command = "cl", search_paths: [])
191
221
  super(search_paths:)
192
222
  resolved_cmd = resolve_command(cl_command)
193
- @c = resolved_cmd
223
+ @c = resolved_cmd
194
224
  setup_msvc_environment(resolved_cmd)
195
225
  end
196
226
 
197
- def command(input_files, output, flags, include_paths, definitions, libs, linker_include_dirs)
227
+ def compile_command(
228
+ input_files,
229
+ flags:,
230
+ include_paths:,
231
+ defs:
232
+ )
198
233
  inc_flags = include_paths.map { |p| "/I#{p}" }
199
- def_flags = definitions.map { |d| "/D#{d}" }
200
-
201
- if flags.include?("/c")
202
- cmd = [c, *flags, *inc_flags, *def_flags, *input_files]
203
- output.nil? ? cmd : [*cmd, "/Fo#{output}"]
204
- else
205
- lib_flags = libs.map { |l| "#{l}.lib" }
206
- lib_path_flags = linker_include_dirs.map { |p| "/LIBPATH:#{p}" }
207
- cmd = [c, *flags, *inc_flags, *def_flags, *input_files, *lib_flags, "/Fe#{output}"]
208
- cmd += ["/link", *lib_path_flags] unless lib_path_flags.empty?
209
- cmd
210
- end
234
+ def_flags = defs.map { |d| "/D#{d}" }
235
+ [c, "/c", *flags, *inc_flags, *def_flags, *input_files]
236
+ end
237
+
238
+ def compile_and_link_commands(
239
+ input_files,
240
+ output_file,
241
+ flags:,
242
+ include_paths:,
243
+ defs:,
244
+ link_paths:,
245
+ libs:
246
+ )
247
+ inc_flags = include_paths.map { |p| "/I#{p}" }
248
+ def_flags = defs.map { |d| "/D#{d}" }
249
+ lib_flags = libs.map { |l| "#{l}.lib" }
250
+ lib_path_flags = link_paths.map { |p| "/LIBPATH:#{p}" }
251
+ cmd = [c, *flags, *inc_flags, *def_flags, *input_files, *lib_flags, "/Fe#{output_file}"]
252
+ cmd += ["/link", *lib_path_flags] unless lib_path_flags.empty?
253
+ [cmd]
211
254
  end
212
255
 
213
256
  MSVC_FLAGS = {
@@ -243,7 +286,6 @@ module MetaCC
243
286
  no_semantic_interposition: [],
244
287
  no_omit_frame_pointer: ["/Oy-"],
245
288
  no_strict_aliasing: [],
246
- objects: ["/c"],
247
289
  shared: ["/LD"],
248
290
  static: ["/c"],
249
291
  strip: []
@@ -285,70 +327,55 @@ module MetaCC
285
327
  def setup_msvc_environment(cl_command)
286
328
  return if command_available?(cl_command)
287
329
 
288
- devenv_path = run_vswhere("-path", "-property", "productPath") ||
289
- run_vswhere("-latest", "-prerelease", "-property", "productPath")
330
+ devenv_path = MSVC.vswhere("-path", "-property", "productPath") ||
331
+ MSVC.vswhere("-latest", "-prerelease", "-property", "productPath")
290
332
  return unless devenv_path
291
333
 
292
- vcvarsall = find_vcvarsall(devenv_path)
293
- return unless vcvarsall
294
-
295
- run_vcvarsall(vcvarsall)
334
+ MSVC.vcvarsall(devenv_path)
296
335
  end
297
336
 
298
337
  # Runs vswhere.exe with the given arguments and returns the trimmed stdout,
299
338
  # or nil if vswhere.exe is absent, the command fails, or produces no output.
300
- def run_vswhere(*args)
301
- return nil unless File.exist?(VSWHERE_PATH)
302
-
303
- stdout = IO.popen([VSWHERE_PATH, *args], &:read)
339
+ def self.vswhere(*args)
340
+ path = IO.popen([VSWHERE_PATH, *args], &:read).strip
304
341
  status = $?
305
- return nil unless status.success?
306
342
 
307
- path = stdout.strip
308
- path.empty? ? nil : path
343
+ status.success? && !path.empty? ? path : nil
309
344
  rescue Errno::ENOENT
310
345
  nil
311
346
  end
312
347
 
313
- # Returns the path to vcvarsall.bat for the given devenv.exe path, or nil
348
+ # Runs vcvarsall.bat for the x64 architecture and merges the resulting
349
+ # environment variables into the current process's ENV so that cl.exe
350
+ # and related tools become available on PATH.
351
+ #
352
+ # Finds the path to vcvarsall.bat for the given devenv.exe path, or nil
314
353
  # if it cannot be located. devenv.exe lives at:
315
354
  # <root>\Common7\IDE\devenv.exe
316
355
  # vcvarsall.bat lives at:
317
356
  # <root>\VC\Auxiliary\Build\vcvarsall.bat
318
- def find_vcvarsall(devenv_path)
357
+ #
358
+ # Parses the output of `vcvarsall.bat … && set` and merges the resulting
359
+ # environment variables into the current process's ENV.
360
+ def MSVC.vcvarsall(devenv_path)
361
+ # See https://stackoverflow.com/a/19929778
362
+ return if ENV.has_key?("DevEnvDir")
363
+
364
+ # Calculate the location of vcvarsall.bat
319
365
  install_root = File.expand_path("../../..", devenv_path)
366
+
367
+ # Check if a file is actually present there
320
368
  vcvarsall = File.join(install_root, "VC", "Auxiliary", "Build", "vcvarsall.bat")
321
- File.exist?(vcvarsall) ? vcvarsall : nil
322
- end
369
+ return unless File.exist?(vcvarsall)
323
370
 
324
- # Runs vcvarsall.bat for the x64 architecture and merges the resulting
325
- # environment variables into the current process's ENV so that cl.exe
326
- # and related tools become available on PATH.
327
- def run_vcvarsall(vcvarsall)
328
- stdout = IO.popen(["cmd.exe", "/c", vcvarsall_command(vcvarsall)], &:read)
371
+ # Run vcvarsall.bat and dump the environment to the shell
372
+ output = `"#{vcvarsall}" x64 && set`
329
373
  status = $?
330
374
  return unless status.success?
331
375
 
332
- load_vcvarsall(stdout)
333
- end
334
-
335
- # Builds the cmd.exe command string for calling vcvarsall.bat and capturing
336
- # the resulting environment variables. The path is double-quoted to handle
337
- # spaces; any embedded double quotes are escaped by doubling them, which is
338
- # the cmd.exe convention inside a double-quoted string. Shellwords is not
339
- # used here because it produces POSIX sh escaping, which is incompatible
340
- # with cmd.exe syntax.
341
- def vcvarsall_command(vcvarsall)
342
- quoted = '"' + vcvarsall.gsub('"', '""') + '"'
343
- "#{quoted} x64 && set"
344
- end
345
-
346
- # Parses the output of `vcvarsall.bat … && set` and merges the resulting
347
- # environment variables into the current process's ENV.
348
- def load_vcvarsall(output)
349
376
  output.each_line do |line|
350
- key, sep, value = line.chomp.partition("=")
351
- next if sep.empty?
377
+ key, value = line.chomp.split("=", 2)
378
+ next if value.to_s.empty?
352
379
 
353
380
  ENV[key] = value
354
381
  end
@@ -376,11 +403,19 @@ module MetaCC
376
403
  end
377
404
 
378
405
  # TinyCC toolchain (tcc). TinyCC only supports C, not C++.
379
- class TinyCC < Toolchain
406
+ class TinyCC < GNU
380
407
 
381
408
  def initialize(search_paths: [])
382
- super
383
- @c = resolve_command("tcc")
409
+ super("tcc", search_paths:)
410
+ @ar = resolve_command("ar")
411
+ end
412
+
413
+ def compile_and_link_commands(input_files, output_file, **options)
414
+ commands = super(input_files, output_file, **options)
415
+ if options[:flags].include?(:static)
416
+ object_files = input_files.map { |f| f.sub(/\.c\z/, ".o") }
417
+ commands << [@ar, "rcs", output_file, *object_files]
418
+ end
384
419
  end
385
420
 
386
421
  # TinyCC does not support C++.
@@ -388,14 +423,8 @@ module MetaCC
388
423
  [:c]
389
424
  end
390
425
 
391
- def command(input_files, output, flags, include_paths, definitions, libs, linker_include_dirs)
392
- inc_flags = include_paths.map { |p| "-I#{p}" }
393
- def_flags = definitions.map { |d| "-D#{d}" }
394
- link_mode = !flags.include?("-c")
395
- lib_path_flags = link_mode ? linker_include_dirs.map { |p| "-L#{p}" } : []
396
- lib_flags = link_mode ? libs.map { |l| "-l#{l}" } : []
397
- cmd = [c, *flags, *inc_flags, *def_flags, *input_files, *lib_path_flags, *lib_flags]
398
- output.nil? ? cmd : [*cmd, "-o", output]
426
+ def version_banner
427
+ IO.popen([c, "-v", { err: :out }], &:read)
399
428
  end
400
429
 
401
430
  TINYCC_FLAGS = {
@@ -431,7 +460,6 @@ module MetaCC
431
460
  no_semantic_interposition: [],
432
461
  no_omit_frame_pointer: [],
433
462
  no_strict_aliasing: [],
434
- objects: ["-c"],
435
463
  shared: ["-shared"],
436
464
  static: ["-c"],
437
465
  strip: []
@@ -0,0 +1,5 @@
1
+ module MetaCC
2
+
3
+ VERSION = "0.2.0"
4
+
5
+ end
data/lib/metacc.rb CHANGED
@@ -1,9 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "metacc/driver"
4
-
5
- module MetaCC
6
-
7
- VERSION = "0.1.0"
8
-
9
- end
4
+ require "metacc/version"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: metacc
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Praneeth Sadda
@@ -23,9 +23,13 @@ files:
23
23
  - lib/metacc/cli.rb
24
24
  - lib/metacc/driver.rb
25
25
  - lib/metacc/toolchain.rb
26
+ - lib/metacc/version.rb
27
+ homepage: https://github.com/psadda/metacc
26
28
  licenses:
27
29
  - BSD-3-Clause
28
30
  metadata:
31
+ homepage_uri: https://github.com/psadda/metacc
32
+ source_code_uri: https://github.com/psadda/metacc
29
33
  rubygems_mfa_required: 'true'
30
34
  rdoc_options: []
31
35
  require_paths:
@@ -34,7 +38,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
34
38
  requirements:
35
39
  - - ">="
36
40
  - !ruby/object:Gem::Version
37
- version: '3.2'
41
+ version: 3.2.0
38
42
  required_rubygems_version: !ruby/object:Gem::Requirement
39
43
  requirements:
40
44
  - - ">="