metacc 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3f277277e6c19c84a200cf69d21452da422980b3955c895bc148666b2e170d7c
4
+ data.tar.gz: e17a0627af2e670202be743c4e745a6aedaf10284ead45cd7c6740dd64dcf4b0
5
+ SHA512:
6
+ metadata.gz: d889eff550e1f704c4e4264d750c1d03ddd7e040c1303eb6147501de5b0b2eec970c595cf183afe6fe7da91ee9465c0aa70f7596887018d3b9f4e979801e9f27
7
+ data.tar.gz: 9a0ae1d9c54d117d809dc236396ee0d4a69b321161a535287195474cdd6845a78dc0352bfe0b51f80487dbe25d9194a2afb98320ed2c90ff458f857d2be92f53
data/bin/metacc ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $LOAD_PATH.unshift(File.join(__dir__, "..", "lib"))
5
+
6
+ require "metacc/cli"
7
+
8
+ MetaCC::CLI.new.run(ARGV)
data/lib/metacc/cli.rb ADDED
@@ -0,0 +1,217 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "optparse"
4
+ require_relative "driver"
5
+
6
+ module MetaCC
7
+
8
+ # Command-line interface for the MetaCC Driver.
9
+ #
10
+ # Usage:
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
+ class CLI
42
+
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
+ WARNING_CONFIGS = {
59
+ "all" => :warn_all,
60
+ "error" => :warn_error
61
+ }
62
+
63
+ TARGETS = {
64
+ "sse4.2" => :sse4_2,
65
+ "avx" => :avx,
66
+ "avx2" => :avx2,
67
+ "avx512" => :avx512,
68
+ "native" => :native
69
+ }.freeze
70
+
71
+ STANDARDS = {
72
+ "c11" => :c11,
73
+ "c17" => :c17,
74
+ "c23" => :c23,
75
+ "c++11" => :cxx11,
76
+ "c++14" => :cxx14,
77
+ "c++17" => :cxx17,
78
+ "c++20" => :cxx20,
79
+ "c++23" => :cxx23,
80
+ "c++26" => :cxx26
81
+ }.freeze
82
+
83
+ # Maps --x<name> CLI option names to xflags toolchain-class keys.
84
+ XFLAGS = {
85
+ "xmsvc" => MSVC,
86
+ "xgnu" => GNU,
87
+ "xclang" => Clang,
88
+ "xclangcl" => ClangCL
89
+ }.freeze
90
+
91
+ def run(argv, driver: Driver.new)
92
+ options, input_paths = parse_compile_args(argv, driver:)
93
+ 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)
97
+ end
98
+
99
+ # Parses compile arguments.
100
+ # Returns [options_hash, remaining_positional_args].
101
+ def parse_compile_args(argv, driver: nil)
102
+ options = {
103
+ include_paths: [],
104
+ defs: [],
105
+ linker_paths: [],
106
+ libs: [],
107
+ output_path: nil,
108
+ run: false,
109
+ flags: [],
110
+ xflags: {},
111
+ }
112
+ parser = OptionParser.new
113
+ setup_compile_options(parser, options, driver:)
114
+ sources = parser.permute(argv)
115
+ [options, sources]
116
+ end
117
+
118
+ private
119
+
120
+ def setup_compile_options(parser, options, driver: nil)
121
+ parser.on("-o FILEPATH", "Output file path") do |value|
122
+ options[:output_path] = value
123
+ end
124
+ parser.on("-I DIRPATH", "Add an include search directory") do |value|
125
+ options[:include_paths] << value
126
+ end
127
+ parser.on("-D DEF", "Add a preprocessor definition") do |value|
128
+ options[:defs] << value
129
+ 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
135
+ end
136
+ parser.on("-m", "--arch ARCH", "Target architecture") do |value|
137
+ options[:flags] << TARGETS[v]
138
+ end
139
+ parser.on("-g", "--debug", "Emit debugging symbols") do
140
+ options[:flags] << :debug
141
+ end
142
+ parser.on("--std STANDARD", "Specify the language standard") do |value|
143
+ options[:flags] << STANDARDS[v]
144
+ end
145
+ parser.on("-W OPTION", "Configure warnings") do |value|
146
+ options[:flags] << WARNING_CONFIGS[v]
147
+ end
148
+ parser.on("-c", "--objects", "Produce object files") do
149
+ options[:flags] << :objects
150
+ end
151
+ parser.on("-r", "--run", "Run the compiled executable after a successful build") do
152
+ options[:run] = true
153
+ end
154
+ parser.on("-l LIB", "Link against library LIB") do |value|
155
+ options[:libs] << value
156
+ end
157
+ parser.on("-L DIR", "Add linker library search path") do |value|
158
+ options[:linker_paths] << value
159
+ end
160
+ parser.on("--shared", "Produce a shared library") do
161
+ options[:flags] << :shared
162
+ end
163
+ parser.on("--static", "Produce a static library") do
164
+ options[:flags] << :static
165
+ end
166
+ parser.on("-s", "--strip", "Strip unneeded symbols") do
167
+ options[:flags] << :strip
168
+ 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
178
+ end
179
+ end
180
+ parser.on_tail("--version", "Print the toolchain version and exit") do
181
+ puts driver&.toolchain&.version_banner
182
+ exit
183
+ end
184
+ end
185
+
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
192
+ end
193
+
194
+ unless objects || output_path
195
+ warn "error: -o is required"
196
+ exit 1
197
+ end
198
+
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
202
+ end
203
+ end
204
+
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
213
+ end
214
+
215
+ end
216
+
217
+ end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "toolchain"
4
+
5
+ module MetaCC
6
+
7
+ # Raised when no supported C/C++ compiler can be found on the system.
8
+ class CompilerNotFoundError < StandardError; end
9
+
10
+ # Driver wraps C and C++ compile and link operations using the first
11
+ # available compiler found on the system (Clang, GCC, or MSVC).
12
+ class Driver
13
+
14
+ RECOGNIZED_FLAGS = Set.new(
15
+ %i[
16
+ o0 o1 o2 o3 os
17
+ sse4_2 avx avx2 avx512 native
18
+ debug lto
19
+ warn_all warn_error
20
+ c11 c17 c23
21
+ 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
26
+ ]
27
+ ).freeze
28
+
29
+ # The detected toolchain (a Toolchain subclass instance).
30
+ attr_reader :toolchain
31
+
32
+ # Detects the first available C/C++ compiler toolchain.
33
+ #
34
+ # @param prefer [Array<Class>] toolchain classes to probe, in priority order.
35
+ # Each element must be a Class derived from Toolchain.
36
+ # Defaults to [Clang, GNU, MSVC].
37
+ # @param search_paths [Array<String>] directories to search for toolchain executables
38
+ # 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: [])
42
+ @toolchain = select_toolchain!(prefer, search_paths)
43
+ end
44
+
45
+ # Invokes the compiler driver for the given input files and output path.
46
+ # 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.
49
+ #
50
+ # @param input_files [String, Array<String>] paths to the input files
51
+ # @param output_path [String] path for the resulting output file
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 libs [Array<String>] library names to link (e.g. "m", "pthread")
57
+ # @param linker_paths [Array<String>] linker library search paths (-L / /LIBPATH:)
58
+ # @param env [Hash] environment variables to set for the subprocess
59
+ # @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(
65
+ input_files,
66
+ output_path,
67
+ flags: [],
68
+ xflags: {},
69
+ include_paths: [],
70
+ defs: [],
71
+ libs: [],
72
+ linker_paths: [],
73
+ env: {},
74
+ working_dir: "."
75
+ )
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?
80
+
81
+ input_files = Array(input_files)
82
+ flags = translate_flags(flags)
83
+ flags.concat(xflags[@toolchain.class] || [])
84
+
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
87
+ end
88
+
89
+ private
90
+
91
+ def select_toolchain!(candidates, search_paths)
92
+ candidates.each do |toolchain_class|
93
+ toolchain = toolchain_class.new(search_paths:)
94
+ return toolchain if toolchain.available?
95
+ 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
105
+ end
106
+
107
+ def apply_default_extension(path, output_type)
108
+ return path unless File.extname(path).empty?
109
+
110
+ ext = @toolchain.default_extension(output_type)
111
+ ext.empty? ? path : "#{path}#{ext}"
112
+ end
113
+
114
+ def translate_flags(flags)
115
+ unrecognized_flag = flags.find { |flag| !RECOGNIZED_FLAGS.include?(flag) }
116
+ if unrecognized_flag
117
+ raise "#{unrecognized_flag.inspect} is not a known flag"
118
+ end
119
+
120
+ flags.flat_map { |flag| @toolchain.flags[flag] }
121
+ end
122
+
123
+ def run_command(cmd, env: {}, working_dir: ".")
124
+ !!system(env, *cmd, chdir: working_dir, out: File::NULL, err: File::NULL)
125
+ end
126
+
127
+ end
128
+
129
+ end
@@ -0,0 +1,446 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+
5
+ module MetaCC
6
+
7
+ # Base class for compiler toolchains.
8
+ # Subclasses set their own command attributes in +initialize+ by calling
9
+ # +command_available?+ to probe the system, then implement the
10
+ # toolchain-specific flag and command building methods.
11
+ # c – command used to compile source files
12
+ class Toolchain
13
+
14
+ attr_reader :c
15
+
16
+ def initialize(search_paths: [])
17
+ @search_paths = search_paths
18
+ end
19
+
20
+ # Returns true if this toolchain's primary compiler is present in PATH.
21
+ def available?
22
+ command_available?(c)
23
+ end
24
+
25
+ # Returns the languages supported by this toolchain as an array of symbols.
26
+ # The default implementation returns [:c, :cxx]. Subclasses that only
27
+ # support a subset of languages should override this method.
28
+ def languages
29
+ [:c, :cxx]
30
+ end
31
+
32
+ # Returns true if +command+ is present in PATH, false otherwise.
33
+ # Intentionally ignores the exit status – only ENOENT (not found) matters.
34
+ def command_available?(command)
35
+ return false if command.nil?
36
+
37
+ !system(command, "--version", out: File::NULL, err: File::NULL).nil?
38
+ end
39
+
40
+ # Returns the output of running the compiler with --version.
41
+ def version_banner
42
+ IO.popen([c, "--version", { err: :out }], &:read)
43
+ end
44
+
45
+ # Returns a Hash mapping universal flags to native flags for this toolchain.
46
+ def flags
47
+ raise RuntimeError, "#{self.class}#flags not implemented"
48
+ end
49
+
50
+ # Returns the full command array for the given inputs, output, and flags.
51
+ # The output mode (object files, shared library, static library, or
52
+ # 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"
55
+ end
56
+
57
+ # Returns the default file extension (with leading dot, e.g. ".o") for the
58
+ # given output type on this toolchain/OS combination. Returns an empty
59
+ # string when no extension is conventional (e.g. executables on Unix).
60
+ #
61
+ # @param output_type [:objects, :shared, :static, :executable]
62
+ # @return [String]
63
+ def default_extension(output_type)
64
+ host_os = RbConfig::CONFIG["host_os"]
65
+ case output_type
66
+ when :objects then ".o"
67
+ when :static then ".a"
68
+ when :shared
69
+ if host_os.match?(/mswin|mingw|cygwin/)
70
+ ".dll"
71
+ elsif host_os.match?(/darwin/)
72
+ ".dylib"
73
+ else
74
+ ".so"
75
+ end
76
+ when :executable
77
+ host_os.match?(/mswin|mingw|cygwin/) ? ".exe" : ""
78
+ else
79
+ raise ArgumentError, "unknown output_type: #{output_type.inspect}"
80
+ end
81
+ end
82
+
83
+ private
84
+
85
+ def c_file?(path)
86
+ File.extname(path).downcase == ".c"
87
+ end
88
+
89
+ # Returns the full path to +name+ if it is found (and executable) in one of
90
+ # the configured search paths, otherwise returns +name+ unchanged so that
91
+ # the shell's PATH is used at execution time.
92
+ def resolve_command(name)
93
+ @search_paths.each do |dir|
94
+ full_path = File.join(dir, name)
95
+ return full_path if File.executable?(full_path)
96
+ end
97
+ name
98
+ end
99
+
100
+ end
101
+
102
+ # GNU-compatible toolchain (gcc).
103
+ class GNU < Toolchain
104
+
105
+ def initialize(search_paths: [])
106
+ super
107
+ @c = resolve_command("gcc")
108
+ end
109
+
110
+ def command(input_files, output, flags, include_paths, definitions, libs, linker_include_dirs)
111
+ 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]
118
+ end
119
+
120
+ 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"]
157
+ }.freeze
158
+
159
+ def flags
160
+ GNU_FLAGS
161
+ end
162
+
163
+ end
164
+
165
+ # Clang toolchain – identical command structure to GNU.
166
+ class Clang < GNU
167
+
168
+ def initialize(search_paths: [])
169
+ super
170
+ @c = resolve_command("clang")
171
+ end
172
+
173
+ CLANG_FLAGS = GNU_FLAGS.merge(lto: ["-flto=thin"]).freeze
174
+
175
+ def flags
176
+ CLANG_FLAGS
177
+ end
178
+
179
+ end
180
+
181
+ # Microsoft Visual C++ toolchain.
182
+ class MSVC < Toolchain
183
+
184
+ # Default location of the Visual Studio Installer's vswhere utility.
185
+ VSWHERE_PATH = File.join(
186
+ ENV.fetch("ProgramFiles(x86)", "C:\\Program Files (x86)"),
187
+ "Microsoft Visual Studio", "Installer", "vswhere.exe"
188
+ ).freeze
189
+
190
+ def initialize(cl_command = "cl", search_paths: [])
191
+ super(search_paths:)
192
+ resolved_cmd = resolve_command(cl_command)
193
+ @c = resolved_cmd
194
+ setup_msvc_environment(resolved_cmd)
195
+ end
196
+
197
+ def command(input_files, output, flags, include_paths, definitions, libs, linker_include_dirs)
198
+ 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
211
+ end
212
+
213
+ 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: []
250
+ }.freeze
251
+
252
+ def flags
253
+ MSVC_FLAGS
254
+ end
255
+
256
+ # MSVC and clang-cl always target Windows, so extensions are Windows-specific
257
+ # regardless of the host OS.
258
+ def default_extension(output_type)
259
+ case output_type
260
+ when :objects then ".obj"
261
+ when :static then ".lib"
262
+ when :shared then ".dll"
263
+ when :executable then ".exe"
264
+ else
265
+ raise ArgumentError, "unknown output_type: #{output_type.inspect}"
266
+ end
267
+ end
268
+
269
+ # MSVC prints its version banner to stderr when invoked with no arguments.
270
+ def show_version
271
+ IO.popen([c, { err: :out }], &:read)
272
+ end
273
+
274
+ private
275
+
276
+ # Attempts to configure the MSVC environment using vswhere.exe when cl.exe
277
+ # is not already available on PATH. Tries two vswhere strategies in order:
278
+ #
279
+ # 1. Query vswhere for VS instances whose tools are already on PATH (-path).
280
+ # 2. Query vswhere for the latest VS instance, including prereleases.
281
+ #
282
+ # When a VS instance is found, locates vcvarsall.bat relative to the
283
+ # returned devenv.exe path and runs it so that cl.exe and related tools
284
+ # become available on PATH.
285
+ def setup_msvc_environment(cl_command)
286
+ return if command_available?(cl_command)
287
+
288
+ devenv_path = run_vswhere("-path", "-property", "productPath") ||
289
+ run_vswhere("-latest", "-prerelease", "-property", "productPath")
290
+ return unless devenv_path
291
+
292
+ vcvarsall = find_vcvarsall(devenv_path)
293
+ return unless vcvarsall
294
+
295
+ run_vcvarsall(vcvarsall)
296
+ end
297
+
298
+ # Runs vswhere.exe with the given arguments and returns the trimmed stdout,
299
+ # 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)
304
+ status = $?
305
+ return nil unless status.success?
306
+
307
+ path = stdout.strip
308
+ path.empty? ? nil : path
309
+ rescue Errno::ENOENT
310
+ nil
311
+ end
312
+
313
+ # Returns the path to vcvarsall.bat for the given devenv.exe path, or nil
314
+ # if it cannot be located. devenv.exe lives at:
315
+ # <root>\Common7\IDE\devenv.exe
316
+ # vcvarsall.bat lives at:
317
+ # <root>\VC\Auxiliary\Build\vcvarsall.bat
318
+ def find_vcvarsall(devenv_path)
319
+ install_root = File.expand_path("../../..", devenv_path)
320
+ vcvarsall = File.join(install_root, "VC", "Auxiliary", "Build", "vcvarsall.bat")
321
+ File.exist?(vcvarsall) ? vcvarsall : nil
322
+ end
323
+
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)
329
+ status = $?
330
+ return unless status.success?
331
+
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
+ output.each_line do |line|
350
+ key, sep, value = line.chomp.partition("=")
351
+ next if sep.empty?
352
+
353
+ ENV[key] = value
354
+ end
355
+ end
356
+
357
+ end
358
+
359
+ # clang-cl toolchain – uses clang-cl compiler with MSVC-compatible flags and
360
+ # environment setup.
361
+ class ClangCL < MSVC
362
+
363
+ def initialize(search_paths: [])
364
+ super("clang-cl", search_paths:)
365
+ end
366
+
367
+ CLANG_CL_FLAGS = MSVC_FLAGS.merge(
368
+ o3: ["/Ot"], # Clang-CL treats /Ot as -O3
369
+ lto: ["-flto=thin"]
370
+ ).freeze
371
+
372
+ def flags
373
+ CLANG_CL_FLAGS
374
+ end
375
+
376
+ end
377
+
378
+ # TinyCC toolchain (tcc). TinyCC only supports C, not C++.
379
+ class TinyCC < Toolchain
380
+
381
+ def initialize(search_paths: [])
382
+ super
383
+ @c = resolve_command("tcc")
384
+ end
385
+
386
+ # TinyCC does not support C++.
387
+ def languages
388
+ [:c]
389
+ end
390
+
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]
399
+ end
400
+
401
+ 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: []
438
+ }.freeze
439
+
440
+ def flags
441
+ TINYCC_FLAGS
442
+ end
443
+
444
+ end
445
+
446
+ end
data/lib/metacc.rb ADDED
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "metacc/driver"
4
+
5
+ module MetaCC
6
+
7
+ VERSION = "0.1.0"
8
+
9
+ end
metadata ADDED
@@ -0,0 +1,47 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: metacc
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Praneeth Sadda
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: |
13
+ metacc provides a small set of classes for invoking C/C++ build tools, abstracting
14
+ away differences between compilers.
15
+ email: psadda@gmail.com
16
+ executables:
17
+ - metacc
18
+ extensions: []
19
+ extra_rdoc_files: []
20
+ files:
21
+ - bin/metacc
22
+ - lib/metacc.rb
23
+ - lib/metacc/cli.rb
24
+ - lib/metacc/driver.rb
25
+ - lib/metacc/toolchain.rb
26
+ licenses:
27
+ - BSD-3-Clause
28
+ metadata:
29
+ rubygems_mfa_required: 'true'
30
+ rdoc_options: []
31
+ require_paths:
32
+ - lib
33
+ required_ruby_version: !ruby/object:Gem::Requirement
34
+ requirements:
35
+ - - ">="
36
+ - !ruby/object:Gem::Version
37
+ version: '3.2'
38
+ required_rubygems_version: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ requirements: []
44
+ rubygems_version: 4.0.7
45
+ specification_version: 4
46
+ summary: A small Ruby scripting system for building C and C++ applications
47
+ test_files: []