From 3fa3922cea27026d44aef1cdf3ca92d82adc7ced Mon Sep 17 00:00:00 2001 From: Jon Turney Date: Fri, 14 Apr 2017 13:58:21 +0100 Subject: [PATCH] Support implibs for executables on Windows Add a boolean 'implib' kwarg to executable(). If true, it is permitted to use the returned build target object in link_with: On platforms where this makes sense (e.g. Windows), an implib is generated for the executable and used when linking. Otherwise, it has no effect. (Rather than checking if it is a StaticLibrary or SharedLibary, BuildTarget subclasses gain the is_linkable_target method to test if they can appear in link_with:) Also install any executable implib in a similar way to a shared library implib, i.e. placing the implib in the appropriate place Add tests of: - a shared_module containing a reference to a symbol which is known (at link time) to be provided by the executable - trying to link with non-implib executables (should fail) - installing the implib (This last one needs a little enhancement of the installed file checking as this is the first install test we have which needs to work with either MSVC-style or GCC-style implib filenames) --- docs/markdown/Reference-manual.md | 1 + mesonbuild/backend/backends.py | 16 +++-- mesonbuild/backend/ninjabackend.py | 6 +- mesonbuild/backend/vs2010backend.py | 4 +- mesonbuild/build.py | 56 ++++++++++++++++- run_project_tests.py | 16 ++++- .../meson.build | 24 ++++++++ .../module.c | 16 +++++ .../prog.c | 60 +++++++++++++++++++ .../57 link with executable/meson.build | 4 ++ .../failing/57 link with executable/module.c | 4 ++ .../failing/57 link with executable/prog.c | 5 ++ .../windows/12 exe implib/installed_files.txt | 4 ++ test cases/windows/12 exe implib/meson.build | 7 +++ test cases/windows/12 exe implib/prog.c | 6 ++ 15 files changed, 218 insertions(+), 11 deletions(-) create mode 100644 test cases/common/154 shared module resolving symbol in executable/meson.build create mode 100644 test cases/common/154 shared module resolving symbol in executable/module.c create mode 100644 test cases/common/154 shared module resolving symbol in executable/prog.c create mode 100644 test cases/failing/57 link with executable/meson.build create mode 100644 test cases/failing/57 link with executable/module.c create mode 100644 test cases/failing/57 link with executable/prog.c create mode 100644 test cases/windows/12 exe implib/installed_files.txt create mode 100644 test cases/windows/12 exe implib/meson.build create mode 100644 test cases/windows/12 exe implib/prog.c diff --git a/docs/markdown/Reference-manual.md b/docs/markdown/Reference-manual.md index e45adf67d..261d89894 100644 --- a/docs/markdown/Reference-manual.md +++ b/docs/markdown/Reference-manual.md @@ -248,6 +248,7 @@ Executable supports the following keyword arguments. Note that just like the pos - `name_suffix` the string that will be used as the extension for the target by overriding the default. By default on Windows this is `exe` and on other platforms it is omitted. - `build_by_default` causes, when set to true, to have this target be built by default, that is, when invoking plain `ninja`, the default value is true for all built target types, since 0.38.0 - `override_options` takes an array of strings in the same format as `project`'s `default_options` overriding the values of these options for this target only, since 0.40.0 +- `implib` when set to true, an import library is generated for the executable, used when the returned build target object appears elsewhere in `link_with:`, on platforms where this is meaningful (e.g. Windows), since 0.42.0 The list of `sources`, `objects`, and `dependencies` is always flattened, which means you can freely nest and add lists while creating the final list. As a corollary, the best way to handle a 'disabled dependency' is by assigning an empty list `[]` to it and passing it like any other dependency to the `dependencies:` keyword argument. diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index cadb655e8..f967de0c6 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -140,7 +140,12 @@ class Backend: return os.path.join(self.get_target_dir(target), link_lib) elif isinstance(target, build.StaticLibrary): return os.path.join(self.get_target_dir(target), target.get_filename()) - raise AssertionError('BUG: Tried to link to something that\'s not a library') + elif isinstance(target, build.Executable): + if target.import_filename: + return os.path.join(self.get_target_dir(target), target.get_import_filename()) + else: + return None + raise AssertionError('BUG: Tried to link to {!r} which is not linkable'.format(target)) def get_target_dir(self, target): if self.environment.coredata.get_builtin_option('layout') == 'mirror': @@ -463,12 +468,13 @@ class Backend: def build_target_link_arguments(self, compiler, deps): args = [] for d in deps: - if not isinstance(d, (build.StaticLibrary, build.SharedLibrary)): + if not (d.is_linkable_target()): raise RuntimeError('Tried to link with a non-library target "%s".' % d.get_basename()) + d_arg = self.get_target_filename_for_linking(d) + if not d_arg: + continue if isinstance(compiler, (compilers.LLVMDCompiler, compilers.DmdDCompiler)): - d_arg = '-L' + self.get_target_filename_for_linking(d) - else: - d_arg = self.get_target_filename_for_linking(d) + d_arg = '-L' + d_arg args.append(d_arg) return args diff --git a/mesonbuild/backend/ninjabackend.py b/mesonbuild/backend/ninjabackend.py index 7f974ee98..f10d5164b 100644 --- a/mesonbuild/backend/ninjabackend.py +++ b/mesonbuild/backend/ninjabackend.py @@ -693,7 +693,8 @@ int dummy; # On toolchains/platforms that use an import library for # linking (separate from the shared library with all the # code), we need to install that too (dll.a/.lib). - if isinstance(t, build.SharedLibrary) and t.get_import_filename(): + if (isinstance(t, build.SharedLibrary) or + isinstance(t, build.Executable)) and t.get_import_filename(): if custom_install_dir: # If the DLL is installed into a custom directory, # install the import library into the same place so @@ -2256,6 +2257,9 @@ rule FORTRAN_DEP_HACK # If gui_app, and that's significant on this platform if target.gui_app and hasattr(linker, 'get_gui_app_args'): commands += linker.get_gui_app_args() + # If implib, and that's significant on this platform (i.e. Windows using either GCC or Visual Studio) + if target.import_filename: + commands += linker.gen_import_library_args(os.path.join(target.subdir, target.import_filename)) elif isinstance(target, build.SharedLibrary): if isinstance(target, build.SharedModule): commands += linker.get_std_shared_module_link_args() diff --git a/mesonbuild/backend/vs2010backend.py b/mesonbuild/backend/vs2010backend.py index 57b0437ee..4a92155b6 100644 --- a/mesonbuild/backend/vs2010backend.py +++ b/mesonbuild/backend/vs2010backend.py @@ -951,10 +951,12 @@ class Vs2010Backend(backends.Backend): ofile.text = '$(OutDir)%s' % target.get_filename() subsys = ET.SubElement(link, 'SubSystem') subsys.text = subsystem - if isinstance(target, build.SharedLibrary): + if (isinstance(target, build.SharedLibrary) or + isinstance(target, build.Executable)) and target.get_import_filename(): # DLLs built with MSVC always have an import library except when # they're data-only DLLs, but we don't support those yet. ET.SubElement(link, 'ImportLibrary').text = target.get_import_filename() + if isinstance(target, build.SharedLibrary): # Add module definitions file, if provided if target.vs_module_defs: relpath = os.path.join(down, target.vs_module_defs.rel_to_builddir(self.build_to_src)) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index fb56ceaa9..df24c7fbe 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -74,6 +74,9 @@ known_lib_kwargs.update({'version': True, # Only for shared libs 'rust_crate_type': True, # Only for Rust libs }) +known_exe_kwargs = known_basic_kwargs.copy() +known_exe_kwargs.update({'implib': True, + }) class InvalidArguments(MesonException): pass @@ -841,8 +844,8 @@ You probably should put it in link_with instead.''') for t in flatten(target): if hasattr(t, 'held_object'): t = t.held_object - if not isinstance(t, (StaticLibrary, SharedLibrary)): - raise InvalidArguments('Link target {!r} is not library.'.format(t)) + if not t.is_linkable_target(): + raise InvalidArguments('Link target {!r} is not linkable.'.format(t)) if isinstance(self, SharedLibrary) and isinstance(t, StaticLibrary) and not t.pic: msg = "Can't link non-PIC static library {!r} into shared library {!r}. ".format(t.name, self.name) msg += "Use the 'pic' option to static_library to build with PIC." @@ -986,6 +989,9 @@ You probably should put it in link_with instead.''') return True return False + def is_linkable_target(self): + return False + class Generator: def __init__(self, args, kwargs): @@ -1122,9 +1128,49 @@ class Executable(BuildTarget): self.filename += '.' + self.suffix self.outputs = [self.filename] + # The import library this target will generate + self.import_filename = None + # The import library that Visual Studio would generate (and accept) + self.vs_import_filename = None + # The import library that GCC would generate (and prefer) + self.gcc_import_filename = None + + # if implib:true appears, this target is linkwith:-able, but that only + # means something on Windows platforms. + self.is_linkwithable = False + if 'implib' in kwargs and kwargs['implib']: + self.is_linkwithable = True + if for_windows(is_cross, environment) or for_cygwin(is_cross, environment): + self.vs_import_filename = '{0}.lib'.format(self.name) + self.gcc_import_filename = 'lib{0}.exe.a'.format(self.name) + + if self.get_using_msvc(): + self.import_filename = self.vs_import_filename + else: + self.import_filename = self.gcc_import_filename + def type_suffix(self): return "@exe" + def check_unknown_kwargs(self, kwargs): + self.check_unknown_kwargs_int(kwargs, known_exe_kwargs) + + def get_import_filename(self): + """ + The name of the import library that will be outputted by the compiler + + Returns None if there is no import library required for this platform + """ + return self.import_filename + + def get_import_filenameslist(self): + if self.import_filename: + return [self.vs_import_filename, self.gcc_import_filename] + return [] + + def is_linkable_target(self): + return self.is_linkwithable + class StaticLibrary(BuildTarget): def __init__(self, name, subdir, subproject, is_cross, sources, objects, environment, kwargs): if 'pic' not in kwargs and 'b_staticpic' in environment.coredata.base_options: @@ -1176,6 +1222,9 @@ class StaticLibrary(BuildTarget): else: raise InvalidArguments('Invalid rust_crate_type "{0}": must be a string.'.format(rust_crate_type)) + def is_linkable_target(self): + return True + class SharedLibrary(BuildTarget): def __init__(self, name, subdir, subproject, is_cross, sources, objects, environment, kwargs): self.soversion = None @@ -1405,6 +1454,9 @@ class SharedLibrary(BuildTarget): def type_suffix(self): return "@sha" + def is_linkable_target(self): + return True + # A shared library that is meant to be used with dlopen rather than linking # into something else. class SharedModule(SharedLibrary): diff --git a/run_project_tests.py b/run_project_tests.py index 3420946c3..69a778e9c 100755 --- a/run_project_tests.py +++ b/run_project_tests.py @@ -183,7 +183,7 @@ def get_relative_files_list_from_dir(fromdir): paths.append(path) return paths -def platform_fix_name(fname): +def platform_fix_name(fname, compiler): if '?lib' in fname: if mesonlib.is_cygwin(): fname = re.sub(r'\?lib(.*)\.dll$', r'cyg\1.dll', fname) @@ -195,6 +195,16 @@ def platform_fix_name(fname): if mesonlib.is_windows() or mesonlib.is_cygwin(): return fname + '.exe' + if fname.startswith('?msvc:'): + fname = fname[6:] + if compiler != 'cl': + return None + + if fname.startswith('?gcc:'): + fname = fname[5:] + if compiler == 'cl': + return None + return fname def validate_install(srcdir, installdir, compiler): @@ -210,7 +220,9 @@ def validate_install(srcdir, installdir, compiler): elif os.path.exists(info_file): with open(info_file) as f: for line in f: - expected[platform_fix_name(line.strip())] = False + line = platform_fix_name(line.strip(), compiler) + if line: + expected[line] = False # Check if expected files were found for fname in expected: file_path = os.path.join(installdir, fname) diff --git a/test cases/common/154 shared module resolving symbol in executable/meson.build b/test cases/common/154 shared module resolving symbol in executable/meson.build new file mode 100644 index 000000000..06c990282 --- /dev/null +++ b/test cases/common/154 shared module resolving symbol in executable/meson.build @@ -0,0 +1,24 @@ +project('shared module resolving symbol in executable', 'c') + +# The shared module contains a reference to the symbol 'func_from_executable', +# which is always provided by the executable which loads it. This symbol can be +# resolved at run-time by an ELF loader. But when building PE/COFF objects, all +# symbols must be resolved at link-time, so an implib is generated for the +# executable, and the shared module linked with it. +# +# See testcase 125 for an example of the more complex portability gymnastics +# required if we do not know (at link-time) what provides the symbol. + +link_flags = [] +if host_machine.system() != 'windows' + # Needed to export dynamic symbols from the executable + link_flags += ['-rdynamic'] + # Need to add this manually because Meson won't add it automatically because + # it doesn't know that we are loading a module from the build directory. + link_flags += ['-Wl,-rpath,' + meson.current_build_dir()] +endif + +dl = meson.get_compiler('c').find_library('dl', required: false) +e = executable('prog', 'prog.c', dependencies: dl, implib: true, link_args: link_flags) +m = shared_module('module', 'module.c', link_with: e) +test('test', e, args: m) diff --git a/test cases/common/154 shared module resolving symbol in executable/module.c b/test cases/common/154 shared module resolving symbol in executable/module.c new file mode 100644 index 000000000..64374d590 --- /dev/null +++ b/test cases/common/154 shared module resolving symbol in executable/module.c @@ -0,0 +1,16 @@ +#if defined _WIN32 || defined __CYGWIN__ + #define DLL_PUBLIC __declspec(dllexport) +#else + #if defined __GNUC__ + #define DLL_PUBLIC __attribute__ ((visibility("default"))) + #else + #pragma message ("Compiler does not support symbol visibility.") + #define DLL_PUBLIC + #endif +#endif + +extern int func_from_executable(void); + +int DLL_PUBLIC func(void) { + return func_from_executable(); +} diff --git a/test cases/common/154 shared module resolving symbol in executable/prog.c b/test cases/common/154 shared module resolving symbol in executable/prog.c new file mode 100644 index 000000000..746c19280 --- /dev/null +++ b/test cases/common/154 shared module resolving symbol in executable/prog.c @@ -0,0 +1,60 @@ +#include +#include +#ifdef _WIN32 +#include +#else +#include +#endif + +#if defined _WIN32 || defined __CYGWIN__ + #define DLL_PUBLIC __declspec(dllexport) +#else + #if defined __GNUC__ + #define DLL_PUBLIC __attribute__ ((visibility("default"))) + #else + #pragma message ("Compiler does not support symbol visibility.") + #define DLL_PUBLIC + #endif +#endif + +typedef int (*fptr) (void); + +int DLL_PUBLIC +func_from_executable(void) +{ + return 42; +} + +int +main (int argc, char **argv) +{ + int expected, actual; + fptr importedfunc; + +#ifdef _WIN32 + HMODULE h = LoadLibraryA(argv[1]); +#else + void *h = dlopen(argv[1], RTLD_NOW); +#endif + assert(h != NULL); + +#ifdef _WIN32 + importedfunc = (fptr) GetProcAddress (h, "func"); +#else + importedfunc = (fptr) dlsym(h, "func"); +#endif + assert(importedfunc != NULL); + assert(importedfunc != func_from_executable); + + actual = (*importedfunc)(); + expected = func_from_executable(); + assert(actual == expected); + +#ifdef _WIN32 + FreeLibrary(h); +#else + dlclose(h); +#endif + + return 0; +} diff --git a/test cases/failing/57 link with executable/meson.build b/test cases/failing/57 link with executable/meson.build new file mode 100644 index 000000000..186b3e595 --- /dev/null +++ b/test cases/failing/57 link with executable/meson.build @@ -0,0 +1,4 @@ +project('link with exe', 'c') + +e = executable('prog', 'prog.c') +m = shared_module('module', 'module.c', link_with: e) diff --git a/test cases/failing/57 link with executable/module.c b/test cases/failing/57 link with executable/module.c new file mode 100644 index 000000000..dc0124a24 --- /dev/null +++ b/test cases/failing/57 link with executable/module.c @@ -0,0 +1,4 @@ + +int func(void) { + return 42; +} diff --git a/test cases/failing/57 link with executable/prog.c b/test cases/failing/57 link with executable/prog.c new file mode 100644 index 000000000..f3836d7ba --- /dev/null +++ b/test cases/failing/57 link with executable/prog.c @@ -0,0 +1,5 @@ +int +main (int argc, char **argv) +{ + return 0; +} diff --git a/test cases/windows/12 exe implib/installed_files.txt b/test cases/windows/12 exe implib/installed_files.txt new file mode 100644 index 000000000..d0ea8f6c4 --- /dev/null +++ b/test cases/windows/12 exe implib/installed_files.txt @@ -0,0 +1,4 @@ +usr/bin/prog.exe +usr/bin/prog.pdb +?gcc:usr/lib/libprog.exe.a +?msvc:usr/lib/prog.lib diff --git a/test cases/windows/12 exe implib/meson.build b/test cases/windows/12 exe implib/meson.build new file mode 100644 index 000000000..2393e794c --- /dev/null +++ b/test cases/windows/12 exe implib/meson.build @@ -0,0 +1,7 @@ +project('wintest', 'c') + +# Test that we can produce an implib for an executable on Windows, and that it +# is installed along with the executable + +prog = executable('prog', 'prog.c', install: true, implib: true) +test('wintest', prog) diff --git a/test cases/windows/12 exe implib/prog.c b/test cases/windows/12 exe implib/prog.c new file mode 100644 index 000000000..6d5a9f501 --- /dev/null +++ b/test cases/windows/12 exe implib/prog.c @@ -0,0 +1,6 @@ +#include + +int __declspec(dllexport) +main(int argc, char **argv) { + return 0; +}