Repository: mesonbuild/meson

Issue:
Support compilation of LLVM-IR
.ll => .o

This can be accomplished through either llc or clang.


Discussion:

Resolving patch:
diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py
--- a/mesonbuild/backend/ninjabackend.py
+++ b/mesonbuild/backend/ninjabackend.py
@@ -257,10 +257,14 @@ def get_target_sources(self, target):
 
     # Languages that can mix with C or C++ but don't support unity builds yet
     # because the syntax we use for unity builds is specific to C/++/ObjC/++.
+    # Assembly files cannot be unitified and neither can LLVM IR files
     langs_cant_unity = ('d', 'fortran')
     def get_target_source_can_unity(self, target, source):
         if isinstance(source, File):
             source = source.fname
+        if self.environment.is_llvm_ir(source) or \
+           self.environment.is_assembly(source):
+            return False
         suffix = os.path.splitext(source)[1][1:]
         for lang in self.langs_cant_unity:
             if not lang in target.compilers:
@@ -374,6 +378,9 @@ def generate_target(self, target, outfile):
         # because we need `header_deps` to be fully generated in the above loop.
         for src in generated_source_files:
             src_list.append(src)
+            if self.environment.is_llvm_ir(src):
+                obj_list.append(self.generate_llvm_ir_compile(target, outfile, src))
+                continue
             obj_list.append(self.generate_single_compile(target, outfile, src, True,
                                                          header_deps=header_deps))
 
@@ -409,7 +416,9 @@ def generate_target(self, target, outfile):
         for f, src in target_sources.items():
             if not self.environment.is_header(src):
                 src_list.append(src)
-                if is_unity and self.get_target_source_can_unity(target, src):
+                if self.environment.is_llvm_ir(src):
+                    obj_list.append(self.generate_llvm_ir_compile(target, outfile, src))
+                elif is_unity and self.get_target_source_can_unity(target, src):
                     abs_src = os.path.join(self.environment.get_build_dir(),
                                            src.rel_to_builddir(self.build_to_src))
                     unity_src.append(abs_src)
@@ -1365,6 +1374,36 @@ def generate_fortran_dep_hack(self, outfile):
 '''
         outfile.write(template % cmd)
 
+    def generate_llvm_ir_compile_rule(self, compiler, is_cross, outfile):
+        if getattr(self, 'created_llvm_ir_rule', False):
+            return
+        rule = 'rule llvm_ir{}_COMPILER\n'.format('_CROSS' if is_cross else '')
+        args = [' '.join([ninja_quote(i) for i in compiler.get_exelist()]),
+                ' '.join(self.get_cross_info_lang_args(compiler, is_cross)),
+                ' '.join(compiler.get_output_args('$out')),
+                ' '.join(compiler.get_compile_only_args())]
+        if mesonlib.is_windows():
+            command_template = ' command = {} @$out.rsp\n' \
+                               ' rspfile = $out.rsp\n' \
+                               ' rspfile_content = {} $ARGS {} {} $in\n'
+        else:
+            command_template = ' command = {} {} $ARGS {} {} $in\n'
+        command = command_template.format(*args)
+        description = ' description = Compiling LLVM IR object $in.\n'
+        outfile.write(rule)
+        outfile.write(command)
+        outfile.write(description)
+        outfile.write('\n')
+        self.created_llvm_ir_rule = True
+
+    def get_cross_info_lang_args(self, lang, is_cross):
+        if is_cross:
+            try:
+                return self.environment.cross_info.config['properties'][lang + '_args']
+            except KeyError:
+                pass
+        return []
+
     def generate_compile_rule_for(self, langname, compiler, qstr, is_cross, outfile):
         if langname == 'java':
             if not is_cross:
@@ -1399,12 +1438,7 @@ def generate_compile_rule_for(self, langname, compiler, qstr, is_cross, outfile)
             if d != '$out' and d != '$in':
                 d = qstr % d
             quoted_depargs.append(d)
-        cross_args = []
-        if is_cross:
-            try:
-                cross_args = self.environment.cross_info.config['properties'][langname + '_args']
-            except KeyError:
-                pass
+        cross_args = self.get_cross_info_lang_args(langname, is_cross)
         if mesonlib.is_windows():
             command_template = ''' command = %s @$out.rsp
  rspfile = $out.rsp
@@ -1477,6 +1511,8 @@ def generate_compile_rules(self, outfile):
         qstr = quote_char + "%s" + quote_char
         for compiler in self.build.compilers:
             langname = compiler.get_language()
+            if compiler.get_id() == 'clang':
+                self.generate_llvm_ir_compile_rule(compiler, False, outfile)
             self.generate_compile_rule_for(langname, compiler, qstr, False, outfile)
             self.generate_pch_rule_for(langname, compiler, qstr, False, outfile)
         if self.environment.is_cross_build():
@@ -1488,6 +1524,8 @@ def generate_compile_rules(self, outfile):
                 cclist = self.build.compilers
             for compiler in cclist:
                 langname = compiler.get_language()
+                if compiler.get_id() == 'clang':
+                    self.generate_llvm_ir_compile_rule(compiler, True, outfile)
                 self.generate_compile_rule_for(langname, compiler, qstr, True, outfile)
                 self.generate_pch_rule_for(langname, compiler, qstr, True, outfile)
         outfile.write('\n')
@@ -1680,9 +1718,39 @@ def get_compile_debugfile_args(self, compiler, target, objfile):
     def get_link_debugfile_args(self, linker, target, outname):
         return linker.get_link_debugfile_args(outname)
 
+    def generate_llvm_ir_compile(self, target, outfile, src):
+        compiler = get_compiler_for_source(target.compilers.values(), src)
+        commands = []
+        # Compiler args for compiling this target
+        commands += compilers.get_base_compile_args(self.environment.coredata.base_options,
+                                                    compiler)
+        if isinstance(src, (RawFilename, File)):
+            src_filename = src.fname
+        elif os.path.isabs(src):
+            src_filename = os.path.basename(src)
+        else:
+            src_filename = src
+        obj_basename = src_filename.replace('/', '_').replace('\\', '_')
+        rel_obj = os.path.join(self.get_target_private_dir(target), obj_basename)
+        rel_obj += '.' + self.environment.get_object_suffix()
+        commands += self.get_compile_debugfile_args(compiler, target, rel_obj)
+        if isinstance(src, RawFilename):
+            rel_src = src.fname
+        elif isinstance(src, File):
+            rel_src = src.rel_to_builddir(self.build_to_src)
+        else:
+            raise InvalidArguments('Invalid source type: {!r}'.format(src))
+        # Write the Ninja build command
+        compiler_name = 'llvm_ir{}_COMPILER'.format('_CROSS' if target.is_cross else '')
+        element = NinjaBuildElement(self.all_outputs, rel_obj, compiler_name, rel_src)
+        commands = self.dedup_arguments(commands)
+        element.add_item('ARGS', commands)
+        element.write(outfile)
+        return rel_obj
+
     def generate_single_compile(self, target, outfile, src, is_generated=False, header_deps=[], order_deps=[]):
         """
-        Compiles C/C++, ObjC/ObjC++, and D sources
+        Compiles C/C++, ObjC/ObjC++, Fortran, and D sources
         """
         if isinstance(src, str) and src.endswith('.h'):
             raise AssertionError('BUG: sources should not contain headers')
diff --git a/mesonbuild/compilers.py b/mesonbuild/compilers.py
--- a/mesonbuild/compilers.py
+++ b/mesonbuild/compilers.py
@@ -59,6 +59,16 @@ def is_source(fname):
     suffix = fname.split('.')[-1]
     return suffix in clike_suffixes
 
+def is_assembly(fname):
+    if hasattr(fname, 'fname'):
+        fname = fname.fname
+    return fname.split('.')[-1].lower() == 's'
+
+def is_llvm_ir(fname):
+    if hasattr(fname, 'fname'):
+        fname = fname.fname
+    return fname.split('.')[-1] == 'll'
+
 def is_object(fname):
     if hasattr(fname, 'fname'):
         fname = fname.fname
@@ -2018,6 +2028,8 @@ def __init__(self, gcc_type, defines):
         if self.gcc_type != GCC_OSX:
             self.base_options.append('b_lundef')
             self.base_options.append('b_asneeded')
+        # All GCC backends can do assembly
+        self.can_compile_suffixes.add('s')
 
     def get_colorout_args(self, colortype):
         if mesonlib.version_compare(self.version, '>=4.9.0'):
@@ -2071,8 +2083,6 @@ class GnuCCompiler(GnuCompiler, CCompiler):
     def __init__(self, exelist, version, gcc_type, is_cross, exe_wrapper=None, defines=None):
         CCompiler.__init__(self, exelist, version, is_cross, exe_wrapper)
         GnuCompiler.__init__(self, gcc_type, defines)
-        # Gcc can do asm, too.
-        self.can_compile_suffixes.add('s')
         self.warn_args = {'1': ['-Wall', '-Winvalid-pch'],
                           '2': ['-Wall', '-Wextra', '-Winvalid-pch'],
                           '3' : ['-Wall', '-Wpedantic', '-Wextra', '-Winvalid-pch']}
@@ -2185,6 +2195,8 @@ def __init__(self, clang_type):
         if self.clang_type != CLANG_OSX:
             self.base_options.append('b_lundef')
             self.base_options.append('b_asneeded')
+        # All Clang backends can do assembly and LLVM IR
+        self.can_compile_suffixes.update(['ll', 's'])
 
     def get_pic_args(self):
         if self.clang_type in (CLANG_WIN, CLANG_OSX):
@@ -2240,8 +2252,6 @@ class ClangCCompiler(ClangCompiler, CCompiler):
     def __init__(self, exelist, version, clang_type, is_cross, exe_wrapper=None):
         CCompiler.__init__(self, exelist, version, is_cross, exe_wrapper)
         ClangCompiler.__init__(self, clang_type)
-        # Clang can do asm, too.
-        self.can_compile_suffixes.add('s')
         self.warn_args = {'1': ['-Wall', '-Winvalid-pch'],
                           '2': ['-Wall', '-Wextra', '-Winvalid-pch'],
                           '3' : ['-Wall', '-Wpedantic', '-Wextra', '-Winvalid-pch']}
diff --git a/mesonbuild/environment.py b/mesonbuild/environment.py
--- a/mesonbuild/environment.py
+++ b/mesonbuild/environment.py
@@ -271,6 +271,12 @@ def is_header(self, fname):
     def is_source(self, fname):
         return is_source(fname)
 
+    def is_assembly(self, fname):
+        return is_assembly(fname)
+
+    def is_llvm_ir(self, fname):
+        return is_llvm_ir(fname)
+
     def is_object(self, fname):
         return is_object(fname)
 


Test patch:
diff --git a/test cases/common/126 llvm ir and assembly/main.c b/test cases/common/126 llvm ir and assembly/main.c
new file mode 100644
--- /dev/null
+++ b/test cases/common/126 llvm ir and assembly/main.c	
@@ -0,0 +1,9 @@
+unsigned square_unsigned (unsigned a);
+
+int
+main (int argc, char * argv[])
+{
+  if (square_unsigned (2) != 4)
+    return -1;
+  return 0;
+}
diff --git a/test cases/common/126 llvm ir and assembly/main.cpp b/test cases/common/126 llvm ir and assembly/main.cpp
new file mode 100644
--- /dev/null
+++ b/test cases/common/126 llvm ir and assembly/main.cpp	
@@ -0,0 +1,12 @@
+
+extern "C" {
+  unsigned square_unsigned (unsigned a);
+}
+
+int
+main (int argc, char * argv[])
+{
+  if (square_unsigned (2) != 4)
+    return -1;
+  return 0;
+}
diff --git a/test cases/common/126 llvm ir and assembly/meson.build b/test cases/common/126 llvm ir and assembly/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/126 llvm ir and assembly/meson.build	
@@ -0,0 +1,17 @@
+project('llvm-ir', 'c', 'cpp')
+
+foreach lang : ['c', 'cpp']
+  cc_id = meson.get_compiler(lang).get_id()
+  if cc_id == 'clang'
+    e = executable('square_ir_' + lang, 'square.ll', 'main.' + lang)
+    test('test IR square' + lang, e)
+  endif
+  # FIXME: msvc does not support passing assembly to cl.exe, but you can pass
+  # it to ml.exe and get a compiled object. Meson should add support for
+  # transparently building assembly with ml.exe with MSVC.
+  if cc_id != 'msvc'
+    cpu = host_machine.cpu_family()
+    e = executable('square_asm_' + lang, 'square-' + cpu + '.S', 'main.' + lang)
+    test('test ASM square' + lang, e, args : [e.full_path()])
+  endif
+endforeach
diff --git a/test cases/common/126 llvm ir and assembly/square-arm.S b/test cases/common/126 llvm ir and assembly/square-arm.S
new file mode 100644
--- /dev/null
+++ b/test cases/common/126 llvm ir and assembly/square-arm.S	
@@ -0,0 +1,9 @@
+#include "symbol-underscore.h"
+
+.text
+.globl SYMBOL_NAME(square_unsigned)
+
+SYMBOL_NAME(square_unsigned):
+	mul	r1, r0, r0
+	mov	r0, r1
+	mov	pc, lr
diff --git a/test cases/common/126 llvm ir and assembly/square-x86.S b/test cases/common/126 llvm ir and assembly/square-x86.S
new file mode 100644
--- /dev/null
+++ b/test cases/common/126 llvm ir and assembly/square-x86.S	
@@ -0,0 +1,9 @@
+#include "symbol-underscore.h"
+
+.text
+.globl SYMBOL_NAME(square_unsigned)
+
+SYMBOL_NAME(square_unsigned):
+	movl	4(%esp), %eax
+	imull	%eax, %eax
+	retl
diff --git a/test cases/common/126 llvm ir and assembly/square-x86_64.S b/test cases/common/126 llvm ir and assembly/square-x86_64.S
new file mode 100644
--- /dev/null
+++ b/test cases/common/126 llvm ir and assembly/square-x86_64.S	
@@ -0,0 +1,9 @@
+#include "symbol-underscore.h"
+
+.text
+.globl SYMBOL_NAME(square_unsigned)
+
+SYMBOL_NAME(square_unsigned):
+	imull	%edi, %edi
+	movl	%edi, %eax
+	retq
diff --git a/test cases/common/126 llvm ir and assembly/square.ll b/test cases/common/126 llvm ir and assembly/square.ll
new file mode 100644
--- /dev/null
+++ b/test cases/common/126 llvm ir and assembly/square.ll	
@@ -0,0 +1,4 @@
+define i32 @square_unsigned(i32 %a) {
+  %1 = mul i32 %a, %a
+  ret i32 %1
+}
diff --git a/test cases/common/126 llvm ir and assembly/symbol-underscore.h b/test cases/common/126 llvm ir and assembly/symbol-underscore.h
new file mode 100644
--- /dev/null
+++ b/test cases/common/126 llvm ir and assembly/symbol-underscore.h	
@@ -0,0 +1,5 @@
+#if defined(__WIN32__) || defined(__APPLE__)
+# define SYMBOL_NAME(name) _##name
+#else
+# define SYMBOL_NAME(name) name
+#endif
diff --git a/test cases/common/127 cpp and asm/meson.build b/test cases/common/127 cpp and asm/meson.build
new file mode 100644
--- /dev/null
+++ b/test cases/common/127 cpp and asm/meson.build	
@@ -0,0 +1,16 @@
+project('c++ and assembly test', 'cpp')
+
+sources = ['trivial.cc']
+# If the compiler cannot compile assembly, don't use it
+if meson.get_compiler('cpp').get_id() != 'msvc'
+  cpu = host_machine.cpu_family()
+  sources += ['retval-' + cpu + '.S']
+  cpp_args = ['-DUSE_ASM']
+  message('Using ASM')
+else
+  cpp_args = ['-DNO_USE_ASM']
+endif
+
+exe = executable('trivialprog', sources,
+  cpp_args : cpp_args)
+test('runtest', exe)
diff --git a/test cases/common/127 cpp and asm/retval-arm.S b/test cases/common/127 cpp and asm/retval-arm.S
new file mode 100644
--- /dev/null
+++ b/test cases/common/127 cpp and asm/retval-arm.S	
@@ -0,0 +1,8 @@
+#include "symbol-underscore.h"
+
+.text
+.globl SYMBOL_NAME(get_retval)
+
+SYMBOL_NAME(get_retval):
+	mov	r0, #0
+	mov	pc, lr
diff --git a/test cases/common/127 cpp and asm/retval-x86.S b/test cases/common/127 cpp and asm/retval-x86.S
new file mode 100644
--- /dev/null
+++ b/test cases/common/127 cpp and asm/retval-x86.S	
@@ -0,0 +1,8 @@
+#include "symbol-underscore.h"
+
+.text
+.globl SYMBOL_NAME(get_retval)
+
+SYMBOL_NAME(get_retval):
+	xorl	%eax, %eax
+	retl
diff --git a/test cases/common/127 cpp and asm/retval-x86_64.S b/test cases/common/127 cpp and asm/retval-x86_64.S
new file mode 100644
--- /dev/null
+++ b/test cases/common/127 cpp and asm/retval-x86_64.S	
@@ -0,0 +1,8 @@
+#include "symbol-underscore.h"
+
+.text
+.globl SYMBOL_NAME(get_retval)
+
+SYMBOL_NAME(get_retval):
+	xorl	%eax, %eax
+	retq
diff --git a/test cases/common/127 cpp and asm/symbol-underscore.h b/test cases/common/127 cpp and asm/symbol-underscore.h
new file mode 100644
--- /dev/null
+++ b/test cases/common/127 cpp and asm/symbol-underscore.h	
@@ -0,0 +1,5 @@
+#if defined(__WIN32__) || defined(__APPLE__)
+# define SYMBOL_NAME(name) _##name
+#else
+# define SYMBOL_NAME(name) name
+#endif
diff --git a/test cases/common/127 cpp and asm/trivial.cc b/test cases/common/127 cpp and asm/trivial.cc
new file mode 100644
--- /dev/null
+++ b/test cases/common/127 cpp and asm/trivial.cc	
@@ -0,0 +1,16 @@
+#include<iostream>
+
+extern "C" {
+  int get_retval(void);
+}
+
+int main(int argc, char **argv) {
+  std::cout << "C++ seems to be working." << std::endl;
+#if defined(USE_ASM)
+  return get_retval();
+#elif defined(NO_USE_ASM)
+  return 0;
+#else
+  #error "Forgot to pass asm define"
+#endif
+}