metacc 0.1.0 → 0.3.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: d74b463f2d62a82dbbe422b47e112052a25c95e8c51b957f01ec2545b2f8f227
4
+ data.tar.gz: 510bdbcba0c031de7909db0a989141afbecd9fa33bcf7b5066cf61d3dacc6f49
5
5
  SHA512:
6
- metadata.gz: d889eff550e1f704c4e4264d750c1d03ddd7e040c1303eb6147501de5b0b2eec970c595cf183afe6fe7da91ee9465c0aa70f7596887018d3b9f4e979801e9f27
7
- data.tar.gz: 9a0ae1d9c54d117d809dc236396ee0d4a69b321161a535287195474cdd6845a78dc0352bfe0b51f80487dbe25d9194a2afb98320ed2c90ff458f857d2be92f53
6
+ metadata.gz: bbfda6722b4f9fa4b59a6be206a1b5ca0503d77f5833dbe5ba07f46525b1f157e4ac5ca7853f0f18ae7efd4f9c2782993a0991e101fd102523323abfeea97dd1
7
+ data.tar.gz: 15f59e0d6f45de8501b38aa6429624dd759d7d7b126ce18c0def60bdd2d56648f4b116483753e2f93da60c3e205ec5fd00813844e59b4975d7e3c81c01cfd590
data/bin/metacc CHANGED
@@ -5,4 +5,16 @@ $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,
11
+ MetaCC::ToolchainNotFoundError,
12
+ OptionParser::AmbiguousOption,
13
+ OptionParser::MissingArgument,
14
+ OptionParser::NeedlessArgument,
15
+ OptionParser::InvalidArgument,
16
+ OptionParser::InvalidOption => e
17
+ warn "#{$0} error: #{e.message}"
18
+ rescue MetaCC::CompileError => e
19
+ warn e.message
20
+ end
data/lib/metacc/cli.rb CHANGED
@@ -9,57 +9,23 @@ module MetaCC
9
9
  #
10
10
  # Usage:
11
11
  # metacc <sources...> -o <output> [options] – compile source file(s)
12
- #
13
- # General:
14
- # -Wall -Werror
15
- # --std=c11 --std=c17 --std=c23
16
- # --std=c++11 --std=c++14 --std=c++17 --std=c++20 --std=c++23 --std=c++26
17
- #
18
- # Linking:
19
- # --objects / -c – compile only; don't link
20
- # -l, -L - specify linker input
21
- # --shared – produce a shared library
22
- # --static – produce a static library
23
- # --lto - enable link time optimization
24
- # --strip / -s – strip unneeded symbols
25
- #
26
- # Code generation:
27
- # -O0, -O1, -O2, -O3 - Set the optimization level
28
- # -msse4.2 -mavx -mavx2 -mavx512 --arch=native - Compile for the given target
29
- # --no-rtti --no-exceptions
30
- # --pic
31
- #
32
- # Debugging:
33
- # --debug / -g
34
- # --asan --ubsan --msan
35
- #
36
- # Toolchain-specific flags (passed to Driver#compile via xflags:):
37
- # --xmsvc VALUE – appended to xflags[MSVC]
38
- # --xgnu VALUE – appended to xflags[GNU]
39
- # --xclang VALUE – appended to xflags[Clang]
40
- # --xclangcl VALUE – appended to xflags[ClangCL]
41
12
  class CLI
42
13
 
43
- # Maps long-form CLI flag names to Driver::RECOGNIZED_FLAGS symbols.
44
- # Optimization-level flags are handled separately via -O LEVEL.
45
- LONG_FLAGS = {
46
- "lto" => :lto,
47
- "asan" => :asan,
48
- "ubsan" => :ubsan,
49
- "msan" => :msan,
50
- "no-rtti" => :no_rtti,
51
- "no-exceptions" => :no_exceptions,
52
- "pic" => :pic,
53
- "no-semantic-interposition" => :no_semantic_interposition,
54
- "no-omit-frame-pointer" => :no_omit_frame_pointer,
55
- "no-strict-aliasing" => :no_strict_aliasing
56
- }.freeze
57
-
58
14
  WARNING_CONFIGS = {
59
15
  "all" => :warn_all,
60
16
  "error" => :warn_error
61
17
  }
62
18
 
19
+ SANITIZERS = {
20
+ "address" => :asan,
21
+ "addr" => :asan,
22
+ "undefined" => :ubsan,
23
+ "ub" => :ubsan,
24
+ "memory" => :msan,
25
+ "mem" => :msan,
26
+ "leak" => :lsan
27
+ }
28
+
63
29
  TARGETS = {
64
30
  "sse4.2" => :sse4_2,
65
31
  "avx" => :avx,
@@ -69,9 +35,9 @@ module MetaCC
69
35
  }.freeze
70
36
 
71
37
  STANDARDS = {
72
- "c11" => :c11,
73
- "c17" => :c17,
74
- "c23" => :c23,
38
+ "c11" => :c11,
39
+ "c17" => :c17,
40
+ "c23" => :c23,
75
41
  "c++11" => :cxx11,
76
42
  "c++14" => :cxx14,
77
43
  "c++17" => :cxx17,
@@ -82,42 +48,52 @@ module MetaCC
82
48
 
83
49
  # Maps --x<name> CLI option names to xflags toolchain-class keys.
84
50
  XFLAGS = {
85
- "xmsvc" => MSVC,
86
- "xgnu" => GNU,
87
- "xclang" => Clang,
88
- "xclangcl" => ClangCL
51
+ "xmsvc" => MSVC,
52
+ "xgnu" => GNU,
53
+ "xclang" => Clang,
54
+ "xclangcl" => ClangCL,
55
+ "xtinycc" => TinyCC
89
56
  }.freeze
90
57
 
91
- def run(argv, driver: Driver.new)
92
- options, input_paths = parse_compile_args(argv, driver:)
58
+ def initialize(driver: Driver.new)
59
+ @driver = driver
60
+ end
61
+
62
+ def run(argv)
63
+ input_paths, options = parse_compile_args(argv)
93
64
  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)
65
+ validate_options!(options[:flags], output_path, link: options[:link], run: options[:run])
66
+ invoke(input_paths, output_path, **options)
97
67
  end
98
68
 
99
69
  # Parses compile arguments.
100
- # Returns [options_hash, remaining_positional_args].
101
- def parse_compile_args(argv, driver: nil)
70
+ # Returns [positional_args, options_hash].
71
+ def parse_compile_args(argv)
102
72
  options = {
103
73
  include_paths: [],
104
- defs: [],
105
- linker_paths: [],
106
- libs: [],
107
- output_path: nil,
108
- run: false,
109
- flags: [],
110
- xflags: {},
74
+ defs: [],
75
+ link: true,
76
+ link_paths: [],
77
+ libs: [],
78
+ output_path: nil,
79
+ run: false,
80
+ flags: [],
81
+ xflags: {}
111
82
  }
112
83
  parser = OptionParser.new
113
- setup_compile_options(parser, options, driver:)
114
- sources = parser.permute(argv)
115
- [options, sources]
84
+ setup_compile_options(parser, options)
85
+ input_paths = parser.permute(argv)
86
+ [input_paths, options]
116
87
  end
117
88
 
118
89
  private
119
90
 
120
- def setup_compile_options(parser, options, driver: nil)
91
+ def setup_compile_options(parser, options)
92
+ parser.require_exact = true
93
+
94
+ parser.separator ""
95
+ parser.separator "General options:"
96
+
121
97
  parser.on("-o FILEPATH", "Output file path") do |value|
122
98
  options[:output_path] = value
123
99
  end
@@ -127,89 +103,131 @@ module MetaCC
127
103
  parser.on("-D DEF", "Add a preprocessor definition") do |value|
128
104
  options[:defs] << value
129
105
  end
130
- parser.on("-O LEVEL", /\A[0-3]\z/, "Optimization level (0–3)") do |level|
131
- options[:flags] << :"o#{l}"
106
+ parser.on("--std=STANDARD", "Specify the language standard") do |value|
107
+ options[:flags] << STANDARDS[value]
132
108
  end
133
- parser.on("-Os", "Optimize for size") do
134
- options[:flags] << :os
109
+ parser.on("-W OPTION", "Configure warnings") do |value|
110
+ options[:flags] << WARNING_CONFIGS[value]
135
111
  end
136
- parser.on("-m", "--arch ARCH", "Target architecture") do |value|
137
- options[:flags] << TARGETS[v]
112
+ parser.on("-r", "--run", "Run the compiled executable after a successful build") do
113
+ options[:run] = true
138
114
  end
139
- parser.on("-g", "--debug", "Emit debugging symbols") do
140
- options[:flags] << :debug
115
+
116
+ parser.separator ""
117
+ parser.separator "Debugging:"
118
+
119
+ parser.on("-g", "--debug-info", "Emit debugging symbols") do
120
+ options[:flags] << :debug_info
141
121
  end
142
- parser.on("--std STANDARD", "Specify the language standard") do |value|
143
- options[:flags] << STANDARDS[v]
122
+ parser.on("-S", "--sanitize SANITIZER", "Enable sanitizer (address, undefined, leak, memory)") do |value|
123
+ options[:flags] << SANITIZERS[value]
144
124
  end
145
- parser.on("-W OPTION", "Configure warnings") do |value|
146
- options[:flags] << WARNING_CONFIGS[v]
125
+
126
+ parser.separator ""
127
+ parser.separator "Optimization:"
128
+
129
+ parser.on("-O LEVEL", /\A[0-3]|s\z/, "Optimization level (0, 1, 2, 3, or s)") do |level|
130
+ options[:flags] << :"o#{level}"
147
131
  end
148
- parser.on("-c", "--objects", "Produce object files") do
149
- options[:flags] << :objects
132
+ parser.on("--lto", "Enable link time optimization") do
133
+ options[:flags] << :lto
150
134
  end
151
- parser.on("-r", "--run", "Run the compiled executable after a successful build") do
152
- options[:run] = true
135
+ parser.on("--omit-frame-pointer") do |value|
136
+ options[:flags] << :omit_frame_pointer
153
137
  end
154
- parser.on("-l LIB", "Link against library LIB") do |value|
155
- options[:libs] << value
138
+ parser.on("--strict-aliasing") do |value|
139
+ options[:flags] << :strict_aliasing
156
140
  end
157
- parser.on("-L DIR", "Add linker library search path") do |value|
158
- options[:linker_paths] << value
141
+
142
+ parser.separator ""
143
+ parser.separator "Code generation:"
144
+
145
+ parser.on("-m", "--arch=ARCH", "Target architecture") do |value|
146
+ options[:flags] << TARGETS[value]
159
147
  end
160
- parser.on("--shared", "Produce a shared library") do
161
- options[:flags] << :shared
148
+ parser.on("--pic", "Generate position independent code") do |value|
149
+ options[:flags] << :pic
150
+ end
151
+ parser.on("--no-rtti", "Disable runtime type information") do |value|
152
+ options[:flags] << :no_rtti
162
153
  end
154
+ parser.on("--no-exceptions", "Disable exceptions (and unwinding info)") do |value|
155
+ options[:flags] << :no_exceptions
156
+ end
157
+
158
+ parser.separator ""
159
+ parser.separator "Linking:"
160
+
163
161
  parser.on("--static", "Produce a static library") do
164
162
  options[:flags] << :static
165
163
  end
164
+ parser.on("--shared", "Produce a shared library") do |value|
165
+ options[:flags] << :shared
166
+ end
167
+ parser.on("--shared-compat", "Produce a shared library with full LD_PRELOAD compatability") do |value|
168
+ options[:flags] << :shared_compat
169
+ end
170
+ parser.on("-c", "Compile only (produce object files without linking)") do
171
+ options[:link] = false
172
+ end
173
+ parser.on("-l LIB", "Link against library LIB") do |value|
174
+ options[:libs] << value
175
+ end
176
+ parser.on("-L DIR", "Add linker library search path") do |value|
177
+ options[:link_paths] << value
178
+ end
179
+
166
180
  parser.on("-s", "--strip", "Strip unneeded symbols") do
167
181
  options[:flags] << :strip
168
182
  end
169
- LONG_FLAGS.each do |name, sym|
170
- parser.on("--#{name}") do
171
- options[:flags] << sym
172
- end
173
- end
174
- XFLAGS.each do |name, tc_class|
175
- parser.on("--#{name} VALUE", "Pass VALUE to the #{tc_class} toolchain") do |value|
176
- options[:xflags][tc_class] ||= []
177
- options[:xflags][tc_class] << value
183
+
184
+ parser.separator ""
185
+ parser.separator "Compiler specific:"
186
+
187
+ XFLAGS.each do |name, toolchain_class|
188
+ toolchain_name = toolchain_class.name.split("::").last
189
+ parser.on("--#{name} FLAG", "Forward FLAG to the compiler if compiling with #{toolchain_name}") do |value|
190
+ options[:xflags][toolchain_class] ||= []
191
+ options[:xflags][toolchain_class] << value
178
192
  end
179
193
  end
194
+
195
+ parser.separator ""
196
+ parser.separator "Informational:"
197
+
180
198
  parser.on_tail("--version", "Print the toolchain version and exit") do
181
- puts driver&.toolchain&.version_banner
199
+ puts @driver.toolchain.version_banner
200
+ exit
201
+ end
202
+ parser.on_tail("-h", "--help", "Show this message") do
203
+ puts parser
182
204
  exit
183
205
  end
184
206
  end
185
207
 
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
208
+ def validate_options!(flags, output_path, link:, run:)
209
+ if !link && output_path
210
+ raise OptionParser::InvalidOption, "cannot specify output path (-o) in compile only mode (-c)"
192
211
  end
193
212
 
194
- unless objects || output_path
195
- warn "error: -o is required"
196
- exit 1
213
+ if link && !output_path
214
+ raise OptionParser::InvalidOption, "must specify an output path (-o)"
197
215
  end
198
216
 
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
217
+ if run && (!link || flags.include?(:shared) || flags.include?(:static))
218
+ raise OptionParser::InvalidOption, "--run may not be used with -c, --shared, or --static"
202
219
  end
203
220
  end
204
221
 
205
- def run_executable(path)
206
- system(path)
207
- end
208
-
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
222
+ def invoke(input_paths, desired_output_path = nil, link: true, run: false, **options)
223
+ if link
224
+ actual_output_path = @driver.compile_and_link(input_paths, desired_output_path, **options)
225
+ system(actual_output_path) if run
226
+ else
227
+ options.delete(:link_paths)
228
+ options.delete(:libs)
229
+ @driver.compile(input_paths, **options)
230
+ end
213
231
  end
214
232
 
215
233
  end
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).
@@ -13,16 +17,16 @@ module MetaCC
13
17
 
14
18
  RECOGNIZED_FLAGS = Set.new(
15
19
  %i[
16
- o0 o1 o2 o3 os
17
- sse4_2 avx avx2 avx512 native
18
- debug lto
19
20
  warn_all warn_error
20
21
  c11 c17 c23
21
22
  cxx11 cxx14 cxx17 cxx20 cxx23 cxx26
22
- asan ubsan msan
23
- no_rtti no_exceptions pic
24
- no_semantic_interposition no_omit_frame_pointer no_strict_aliasing
25
- objects shared static strip
23
+ o0 o1 o2 o3 os lto
24
+ sse4_2 avx avx2 avx512 native
25
+ asan ubsan msan lsan
26
+ debug_info
27
+ omit_frame_pointer strict_aliasing
28
+ no_rtti no_exceptions
29
+ pic shared shared_compat 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)
@@ -117,11 +154,15 @@ module MetaCC
117
154
  raise "#{unrecognized_flag.inspect} is not a known flag"
118
155
  end
119
156
 
157
+ flags << :no_omit_frame_pointer unless flags.include?(:omit_frame_pointer)
158
+ flags << :no_strict_aliasing unless flags.include?(:strict_aliasing)
159
+
120
160
  flags.flat_map { |flag| @toolchain.flags[flag] }
121
161
  end
122
162
 
123
163
  def run_command(cmd, env: {}, working_dir: ".")
124
- !!system(env, *cmd, chdir: working_dir, out: File::NULL, err: File::NULL)
164
+ _out, err, status = Open3.capture3(env, *cmd, chdir: working_dir)
165
+ raise CompileError, err unless status.success?
125
166
  end
126
167
 
127
168
  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,58 +117,78 @@ 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"],
147
- no_rtti: ["-fno-rtti"],
148
- no_exceptions: ["-fno-exceptions", "-fno-unwind-tables"],
149
- pic: ["-fPIC"],
150
- no_semantic_interposition: ["-fno-semantic-interposition"],
151
- no_omit_frame_pointer: ["-fno-omit-frame-pointer"],
152
- no_strict_aliasing: ["-fno-strict-aliasing"],
153
- objects: ["-c"],
154
- shared: ["-shared"],
155
- static: ["-r", "-nostdlib"],
156
- strip: ["-Wl,--strip-unneeded"]
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_info: ["-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"],
179
+ lsan: ["-fsanitize=leak"],
180
+ no_rtti: ["-fno-rtti"],
181
+ no_exceptions: ["-fno-exceptions", "-fno-unwind-tables"],
182
+ pic: ["-fPIC"],
183
+ omit_frame_pointer: ["-fomit-frame-pointer"],
184
+ no_omit_frame_pointer: ["-fno-omit-frame-pointer"],
185
+ strict_aliasing: ["-fstrict-aliasing"],
186
+ no_strict_aliasing: ["-fno-strict-aliasing"],
187
+ shared: ["-shared", "-Bsymbolic-non-weak-functions", "-fno-semantic-interposition"],
188
+ shared_compat: ["-shared"],
189
+ static: ["-static"],
190
+ strip: ["-Wl,--strip-unneeded"],
191
+ debug: ["-D_GLIBCXX_DEBUG", "-fasynchronous-unwind-tables"]
157
192
  }.freeze
158
193
 
159
194
  def flags
@@ -166,8 +201,7 @@ module MetaCC
166
201
  class Clang < GNU
167
202
 
168
203
  def initialize(search_paths: [])
169
- super
170
- @c = resolve_command("clang")
204
+ super("clang", search_paths:)
171
205
  end
172
206
 
173
207
  CLANG_FLAGS = GNU_FLAGS.merge(lto: ["-flto=thin"]).freeze
@@ -190,63 +224,78 @@ module MetaCC
190
224
  def initialize(cl_command = "cl", search_paths: [])
191
225
  super(search_paths:)
192
226
  resolved_cmd = resolve_command(cl_command)
193
- @c = resolved_cmd
227
+ @c = resolved_cmd
194
228
  setup_msvc_environment(resolved_cmd)
195
229
  end
196
230
 
197
- def command(input_files, output, flags, include_paths, definitions, libs, linker_include_dirs)
231
+ def compile_command(
232
+ input_files,
233
+ flags:,
234
+ include_paths:,
235
+ defs:
236
+ )
198
237
  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
238
+ def_flags = defs.map { |d| "/D#{d}" }
239
+ [c, "/c", *flags, *inc_flags, *def_flags, *input_files]
240
+ end
241
+
242
+ def compile_and_link_commands(
243
+ input_files,
244
+ output_file,
245
+ flags:,
246
+ include_paths:,
247
+ defs:,
248
+ link_paths:,
249
+ libs:
250
+ )
251
+ inc_flags = include_paths.map { |p| "/I#{p}" }
252
+ def_flags = defs.map { |d| "/D#{d}" }
253
+ lib_flags = libs.map { |l| "#{l}.lib" }
254
+ lib_path_flags = link_paths.map { |p| "/LIBPATH:#{p}" }
255
+ cmd = [c, *flags, *inc_flags, *def_flags, *input_files, *lib_flags, "/Fe#{output_file}"]
256
+ cmd += ["/link", *lib_path_flags] unless lib_path_flags.empty?
257
+ [cmd]
211
258
  end
212
259
 
213
260
  MSVC_FLAGS = {
214
- o0: ["/Od"],
215
- o1: ["/O1"],
216
- o2: ["/O2"],
217
- o3: ["/O2", "/Ob3"],
218
- os: ["/O1"],
219
- sse4_2: ["/arch:SSE4.2"],
220
- avx: ["/arch:AVX"],
221
- avx2: ["/arch:AVX2"],
222
- avx512: ["/arch:AVX512"],
223
- native: [],
224
- debug: ["/Zi"],
225
- lto: ["/GL"],
226
- warn_all: ["/W4"],
227
- warn_error: ["/WX"],
228
- c11: ["/std:c11"],
229
- c17: ["/std:c17"],
230
- c23: ["/std:clatest"],
231
- cxx11: [],
232
- cxx14: ["/std:c++14"],
233
- cxx17: ["/std:c++17"],
234
- cxx20: ["/std:c++20"],
235
- cxx23: ["/std:c++23preview"],
236
- cxx26: ["/std:c++latest"],
237
- asan: ["/fsanitize=address"],
238
- ubsan: [],
239
- msan: [],
240
- no_rtti: ["/GR-"],
241
- no_exceptions: ["/EHs-", "/EHc-"],
242
- pic: [],
243
- no_semantic_interposition: [],
244
- no_omit_frame_pointer: ["/Oy-"],
245
- no_strict_aliasing: [],
246
- objects: ["/c"],
247
- shared: ["/LD"],
248
- static: ["/c"],
249
- strip: []
261
+ o0: ["/Od"],
262
+ o1: ["/O1"],
263
+ o2: ["/O2"],
264
+ o3: ["/O2", "/Ob3"],
265
+ os: ["/O1"],
266
+ sse4_2: ["/arch:SSE4.2"],
267
+ avx: ["/arch:AVX"],
268
+ avx2: ["/arch:AVX2"],
269
+ avx512: ["/arch:AVX512"],
270
+ native: [],
271
+ debug_info: ["/Zi"],
272
+ lto: ["/GL"],
273
+ warn_all: ["/W4"],
274
+ warn_error: ["/WX"],
275
+ c11: ["/std:c11"],
276
+ c17: ["/std:c17"],
277
+ c23: ["/std:clatest"],
278
+ cxx11: [],
279
+ cxx14: ["/std:c++14"],
280
+ cxx17: ["/std:c++17"],
281
+ cxx20: ["/std:c++20"],
282
+ cxx23: ["/std:c++23preview"],
283
+ cxx26: ["/std:c++latest"],
284
+ asan: ["/fsanitize=address"],
285
+ ubsan: [],
286
+ msan: [],
287
+ lsan: [],
288
+ no_rtti: ["/GR-"],
289
+ no_exceptions: ["/EHs-", "/EHc-"],
290
+ pic: [],
291
+ omit_frame_pointer: ["/Oy"],
292
+ no_omit_frame_pointer: ["/Oy-"],
293
+ strict_aliasing: ["-fstrict-aliasing"],
294
+ no_strict_aliasing: ["-fno-strict-aliasing"],
295
+ shared: ["/LD"],
296
+ shared_compat: ["/LD"],
297
+ static: ["/c"],
298
+ strip: []
250
299
  }.freeze
251
300
 
252
301
  def flags
@@ -285,70 +334,55 @@ module MetaCC
285
334
  def setup_msvc_environment(cl_command)
286
335
  return if command_available?(cl_command)
287
336
 
288
- devenv_path = run_vswhere("-path", "-property", "productPath") ||
289
- run_vswhere("-latest", "-prerelease", "-property", "productPath")
337
+ devenv_path = MSVC.vswhere("-path", "-property", "productPath") ||
338
+ MSVC.vswhere("-latest", "-prerelease", "-property", "productPath")
290
339
  return unless devenv_path
291
340
 
292
- vcvarsall = find_vcvarsall(devenv_path)
293
- return unless vcvarsall
294
-
295
- run_vcvarsall(vcvarsall)
341
+ MSVC.vcvarsall(devenv_path)
296
342
  end
297
343
 
298
344
  # Runs vswhere.exe with the given arguments and returns the trimmed stdout,
299
345
  # 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)
346
+ def self.vswhere(*args)
347
+ path = IO.popen([VSWHERE_PATH, *args], &:read).strip
304
348
  status = $?
305
- return nil unless status.success?
306
349
 
307
- path = stdout.strip
308
- path.empty? ? nil : path
350
+ status.success? && !path.empty? ? path : nil
309
351
  rescue Errno::ENOENT
310
352
  nil
311
353
  end
312
354
 
313
- # Returns the path to vcvarsall.bat for the given devenv.exe path, or nil
355
+ # Runs vcvarsall.bat for the x64 architecture and merges the resulting
356
+ # environment variables into the current process's ENV so that cl.exe
357
+ # and related tools become available on PATH.
358
+ #
359
+ # Finds the path to vcvarsall.bat for the given devenv.exe path, or nil
314
360
  # if it cannot be located. devenv.exe lives at:
315
361
  # <root>\Common7\IDE\devenv.exe
316
362
  # vcvarsall.bat lives at:
317
363
  # <root>\VC\Auxiliary\Build\vcvarsall.bat
318
- def find_vcvarsall(devenv_path)
364
+ #
365
+ # Parses the output of `vcvarsall.bat … && set` and merges the resulting
366
+ # environment variables into the current process's ENV.
367
+ def MSVC.vcvarsall(devenv_path)
368
+ # See https://stackoverflow.com/a/19929778
369
+ return if ENV.has_key?("DevEnvDir")
370
+
371
+ # Calculate the location of vcvarsall.bat
319
372
  install_root = File.expand_path("../../..", devenv_path)
373
+
374
+ # Check if a file is actually present there
320
375
  vcvarsall = File.join(install_root, "VC", "Auxiliary", "Build", "vcvarsall.bat")
321
- File.exist?(vcvarsall) ? vcvarsall : nil
322
- end
376
+ return unless File.exist?(vcvarsall)
323
377
 
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)
378
+ # Run vcvarsall.bat and dump the environment to the shell
379
+ output = `"#{vcvarsall}" x64 && set`
329
380
  status = $?
330
381
  return unless status.success?
331
382
 
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
383
  output.each_line do |line|
350
- key, sep, value = line.chomp.partition("=")
351
- next if sep.empty?
384
+ key, value = line.chomp.split("=", 2)
385
+ next if value.to_s.empty?
352
386
 
353
387
  ENV[key] = value
354
388
  end
@@ -376,11 +410,20 @@ module MetaCC
376
410
  end
377
411
 
378
412
  # TinyCC toolchain (tcc). TinyCC only supports C, not C++.
379
- class TinyCC < Toolchain
413
+ class TinyCC < GNU
380
414
 
381
415
  def initialize(search_paths: [])
382
- super
383
- @c = resolve_command("tcc")
416
+ super("tcc", search_paths:)
417
+ @ar = resolve_command("ar")
418
+ end
419
+
420
+ def compile_and_link_commands(input_files, output_file, **options)
421
+ commands = super(input_files, output_file, **options)
422
+ if options[:flags].include?(:static)
423
+ object_files = input_files.map { |f| f.sub(/\.c\z/, ".o") }
424
+ commands << [@ar, "rcs", output_file, *object_files]
425
+ end
426
+ commands
384
427
  end
385
428
 
386
429
  # TinyCC does not support C++.
@@ -388,53 +431,47 @@ module MetaCC
388
431
  [:c]
389
432
  end
390
433
 
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]
434
+ def version_banner
435
+ IO.popen([c, "-v", { err: :out }], &:read)
399
436
  end
400
437
 
401
438
  TINYCC_FLAGS = {
402
- o0: [],
403
- o1: ["-O1"],
404
- o2: ["-O2"],
405
- o3: ["-O2"],
406
- os: [],
407
- sse4_2: [],
408
- avx: [],
409
- avx2: [],
410
- avx512: [],
411
- native: [],
412
- debug: ["-g"],
413
- lto: [],
414
- warn_all: ["-Wall"],
415
- warn_error: ["-Werror"],
416
- c11: [],
417
- c17: [],
418
- c23: [],
419
- cxx11: [],
420
- cxx14: [],
421
- cxx17: [],
422
- cxx20: [],
423
- cxx23: [],
424
- cxx26: [],
425
- asan: [],
426
- ubsan: [],
427
- msan: [],
428
- no_rtti: [],
429
- no_exceptions: [],
430
- pic: [],
431
- no_semantic_interposition: [],
432
- no_omit_frame_pointer: [],
433
- no_strict_aliasing: [],
434
- objects: ["-c"],
435
- shared: ["-shared"],
436
- static: ["-c"],
437
- strip: []
439
+ o0: [],
440
+ o1: ["-O1"],
441
+ o2: ["-O2"],
442
+ o3: ["-O2"],
443
+ os: [],
444
+ sse4_2: [],
445
+ avx: [],
446
+ avx2: [],
447
+ avx512: [],
448
+ native: [],
449
+ debug_info: ["-g"],
450
+ lto: [],
451
+ warn_all: ["-Wall"],
452
+ warn_error: ["-Werror"],
453
+ c11: [],
454
+ c17: [],
455
+ c23: [],
456
+ cxx11: [],
457
+ cxx14: [],
458
+ cxx17: [],
459
+ cxx20: [],
460
+ cxx23: [],
461
+ cxx26: [],
462
+ asan: [],
463
+ ubsan: [],
464
+ msan: [],
465
+ leak: [],
466
+ no_rtti: [],
467
+ no_exceptions: [],
468
+ pic: [],
469
+ keep_frame_pointer: [],
470
+ relaxed_aliasing: [],
471
+ shared: ["-shared"],
472
+ shared_compat: ["-shared"],
473
+ static: ["-c"],
474
+ strip: []
438
475
  }.freeze
439
476
 
440
477
  def flags
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MetaCC
4
+
5
+ VERSION = "0.3.0"
6
+
7
+ 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.3.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
  - - ">="