From 0143c32c7cd82872e42f57216bb94a26191f2824 Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Fri, 1 Jul 2016 14:43:51 +0530 Subject: Overhaul versioning and naming of libraries This commit contains several changes to the naming and versioning of shared and static libraries. The details are documented at: https://github.com/mesonbuild/meson/pull/417 Here's a brief summary: * The results of binary and compiler detection via environment functions are now cached so that they can be called repeatedly without performance penalty. This is necessary because every build.SharedLibrary object has to know whether the compiler is MSVC or not (output filenames depend on that), and so the compiler detection has to be called for each object instantiation. * Linux shared libraries don't always have a library version. Sometimes only soversions are specified (and vice-versa), so support both. * Don't use versioned filenames when generating DLLs, DLLs are never versioned using the suffix in the way that .so libraries are. Hence, they don't use "aliases". Only Linux shared libraries use those. * OS X dylibs do not use filename aliases at all. They only use the soversion in the dylib name (libfoo.X.dylib), and that's it. If there's no soversion specified, the dylib is called libfoo.dylib. Further versioning in dylibs is supposed to be done with the -current_version argument to clang, but this is TBD. https://developer.apple.com/library/mac/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/DynamicLibraryDesignGuidelines.html#//apple_ref/doc/uid/TP40002013-SW23 * Install DLLs into bindir and import libraries into libdir * Static libraries are now always called libfoo.a, even with MSVC * .lib import libraries are always generated when building with MSVC * .dll.a import libraries are always generated when building with MinGW/GCC or MinGW/clang * TODO: Use dlltool if available to generate .dll.a when .lib is generated and vice-versa. * Library and executable suffix/prefixes are now always correctly overriden by the values of the 'name_prefix' and 'name_suffix' keyword arguments. --- mesonbuild/build.py | 278 +++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 208 insertions(+), 70 deletions(-) (limited to 'mesonbuild/build.py') diff --git a/mesonbuild/build.py b/mesonbuild/build.py index bfdbca8..302f824 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -16,8 +16,9 @@ from . import coredata from . import environment from . import dependencies from . import mlog -import copy, os +import copy, os, re from .mesonlib import File, flatten, MesonException +from .environment import for_windows, for_darwin known_basic_kwargs = {'install' : True, 'c_pch' : True, @@ -71,6 +72,35 @@ We are fully aware that these are not really usable or pleasant ways to do this but it's the best we can do given the way shell quoting works. ''' +def sources_are_suffix(sources, suffix): + return len(sources) > 0 and sources[0].endswith('.' + suffix) + +def compiler_is_msvc(sources, is_cross, env): + """ + Since each target does not currently have the compiler information attached + to it, we must do this detection manually here. + + This detection is purposely incomplete and will cause bugs if other code is + extended and this piece of code is forgotten. + """ + compiler = None + if sources_are_suffix(sources, 'c'): + try: + compiler = env.detect_c_compiler(is_cross) + except MesonException: + return False + elif sources_are_suffix(sources, 'cxx') or \ + sources_are_suffix(sources, 'cpp') or \ + sources_are_suffix(sources, 'cc'): + try: + compiler = env.detect_cpp_compiler(is_cross) + except MesonException: + return False + if compiler and compiler.get_id() == 'msvc' and compiler.can_compile(sources[0]): + return True + return False + + class InvalidArguments(MesonException): pass @@ -670,15 +700,19 @@ class GeneratedList(): class Executable(BuildTarget): def __init__(self, name, subdir, subproject, is_cross, sources, objects, environment, kwargs): super().__init__(name, subdir, subproject, is_cross, sources, objects, environment, kwargs) - self.prefix = '' - self.suffix = environment.get_exe_suffix() - suffix = environment.get_exe_suffix() - if len(self.sources) > 0 and self.sources[0].endswith('.cs'): - suffix = 'exe' - if suffix != '': - self.filename = self.name + '.' + suffix - else: - self.filename = self.name + # Unless overriden, executables have no suffix or prefix. Except on + # Windows and with C#/Mono executables where the suffix is 'exe' + if not hasattr(self, 'prefix'): + self.prefix = '' + if not hasattr(self, 'suffix'): + # Executable for Windows or C#/Mono + if for_windows(is_cross, environment) or sources_are_suffix(self.sources, 'cs'): + self.suffix = 'exe' + else: + self.suffix = '' + self.filename = self.name + if self.suffix: + self.filename += '.' + self.suffix def type_suffix(self): return "@exe" @@ -686,52 +720,161 @@ class Executable(BuildTarget): class StaticLibrary(BuildTarget): def __init__(self, name, subdir, subproject, is_cross, sources, objects, environment, kwargs): super().__init__(name, subdir, subproject, is_cross, sources, objects, environment, kwargs) - if len(self.sources) > 0 and self.sources[0].endswith('.cs'): + if sources_are_suffix(self.sources, 'cs'): raise InvalidArguments('Static libraries not supported for C#.') + # By default a static library is named libfoo.a even on Windows because + # MSVC does not have a consistent convention for what static libraries + # are called. The MSVC CRT uses libfoo.lib syntax but nothing else uses + # it and GCC only looks for static libraries called foo.lib and + # libfoo.a. However, we cannot use foo.lib because that's the same as + # the import library. Using libfoo.a is ok because people using MSVC + # always pass the library filename while linking anyway. if not hasattr(self, 'prefix'): - self.prefix = environment.get_static_lib_prefix() - self.suffix = environment.get_static_lib_suffix() - if len(self.sources) > 0 and self.sources[0].endswith('.rs'): - self.suffix = 'rlib' + self.prefix = 'lib' + if not hasattr(self, 'suffix'): + # Rust static library crates have .rlib suffix + if sources_are_suffix(self.sources, 'rs'): + self.suffix = 'rlib' + else: + self.suffix = 'a' self.filename = self.prefix + self.name + '.' + self.suffix - def get_import_filename(self): - return self.filename - - def get_osx_filename(self): - return self.get_filename() - def type_suffix(self): return "@sta" class SharedLibrary(BuildTarget): def __init__(self, name, subdir, subproject, is_cross, sources, objects, environment, kwargs): - self.version = None self.soversion = None + self.ltversion = None self.vs_module_defs = None - super().__init__(name, subdir, subproject, is_cross, sources, objects, environment, kwargs); - if len(self.sources) > 0 and self.sources[0].endswith('.cs'): + # 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 + super().__init__(name, subdir, subproject, is_cross, sources, objects, environment, kwargs) + if not hasattr(self, 'prefix'): + self.prefix = None + if not hasattr(self, 'suffix'): + self.suffix = None + self.basic_filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + self.determine_filenames(is_cross, environment) + + def determine_filenames(self, is_cross, env): + """ + See https://github.com/mesonbuild/meson/pull/417 for details. + + First we determine the filename template (self.filename_tpl), then we + set the output filename (self.filename). + + The template is needed while creating aliases (self.get_aliaslist), + which are needed while generating .so shared libraries for Linux. + + Besides this, there's also the import library name, which is only used + on Windows since on that platform the linker uses a separate library + called the "import library" during linking instead of the shared + library (DLL). The toolchain will output an import library in one of + two formats: GCC or Visual Studio. + + When we're building with Visual Studio, the import library that will be + generated by the toolchain is self.vs_import_filename, and with + MinGW/GCC, it's self.gcc_import_filename. self.import_filename will + always contain the import library name this target will generate. + """ + prefix = '' + suffix = '' + self.filename_tpl = self.basic_filename_tpl + # If the user already provided the prefix and suffix to us, we don't + # need to do any filename suffix/prefix detection. + # NOTE: manual prefix/suffix override is currently only tested for C/C++ + if self.prefix != None and self.suffix != None: + pass + # C# and Mono + elif sources_are_suffix(self.sources, 'cs'): + prefix = '' + suffix = 'dll' + self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + # Rust + elif sources_are_suffix(self.sources, 'rs'): + # Currently, we always build --crate-type=rlib prefix = 'lib' + suffix = 'rlib' + self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + # C, C++, Swift, Vala + # Only Windows uses a separate import library for linking + # For all other targets/platforms import_filename stays None + elif for_windows(is_cross, env): suffix = 'dll' + self.vs_import_filename = '{0}.lib'.format(self.name) + self.gcc_import_filename = 'lib{0}.dll.a'.format(self.name) + if compiler_is_msvc(self.sources, is_cross, env): + # Shared library is of the form foo.dll + prefix = '' + # Import library is called foo.lib + self.import_filename = self.vs_import_filename + # Assume GCC-compatible naming + else: + # Shared library is of the form libfoo.dll + prefix = 'lib' + # Import library is called libfoo.dll.a + self.import_filename = self.gcc_import_filename + # Shared library has the soversion if it is defined + if self.soversion: + self.filename_tpl = '{0.prefix}{0.name}-{0.soversion}.{0.suffix}' + else: + self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + elif for_darwin(is_cross, env): + prefix = 'lib' + suffix = 'dylib' + if self.soversion: + # libfoo.X.dylib + self.filename_tpl = '{0.prefix}{0.name}.{0.soversion}.{0.suffix}' + else: + # libfoo.dylib + self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' else: - prefix = environment.get_shared_lib_prefix() - suffix = environment.get_shared_lib_suffix() - if not hasattr(self, 'prefix'): - self.prefix = prefix - if not hasattr(self, 'suffix'): - if len(self.sources) > 0 and self.sources[0].endswith('.rs'): - self.suffix = 'rlib' + prefix = 'lib' + suffix = 'so' + if self.ltversion: + # libfoo.so.X[.Y[.Z]] (.Y and .Z are optional) + self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}.{0.ltversion}' + elif self.soversion: + # libfoo.so.X + self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}.{0.soversion}' else: - self.suffix = suffix - self.importsuffix = environment.get_import_lib_suffix() - self.filename = self.prefix + self.name + '.' + self.suffix + # No versioning, libfoo.so + self.filename_tpl = '{0.prefix}{0.name}.{0.suffix}' + if self.prefix == None: + self.prefix = prefix + if self.suffix == None: + self.suffix = suffix + self.filename = self.filename_tpl.format(self) def process_kwargs(self, kwargs, environment): super().process_kwargs(kwargs, environment) + # Shared library version if 'version' in kwargs: - self.set_version(kwargs['version']) + self.ltversion = kwargs['version'] + if not isinstance(self.ltversion, str): + raise InvalidArguments('Shared library version needs to be a string, not ' + type(self.ltversion).__name__) + if not re.fullmatch(r'[0-9]+(\.[0-9]+){0,2}', self.ltversion): + raise InvalidArguments('Invalid Shared library version "{0}". Must be of the form X.Y.Z where all three are numbers. Y and Z are optional.'.format(self.ltversion)) + # Try to extract/deduce the soversion if 'soversion' in kwargs: - self.set_soversion(kwargs['soversion']) + self.soversion = kwargs['soversion'] + if isinstance(self.soversion, int): + self.soversion = str(self.soversion) + if not isinstance(self.soversion, str): + raise InvalidArguments('Shared library soversion is not a string or integer.') + try: + int(self.soversion) + except ValueError: + raise InvalidArguments('Shared library soversion must be a valid integer') + elif self.ltversion: + # library version is defined, get the soversion from that + self.soversion = self.ltversion.split('.')[0] + # Visual Studio module-definitions file if 'vs_module_defs' in kwargs: path = kwargs['vs_module_defs'] if (os.path.isabs(path)): @@ -742,46 +885,41 @@ class SharedLibrary(BuildTarget): def check_unknown_kwargs(self, kwargs): self.check_unknown_kwargs_int(kwargs, known_shlib_kwargs) - def get_shbase(self): - return self.prefix + self.name + '.' + self.suffix - def get_import_filename(self): - return self.prefix + self.name + '.' + self.importsuffix + """ + The name of the import library that will be outputted by the compiler - def get_all_link_deps(self): - return [self] + self.get_transitive_link_deps() + Returns None if there is no import library required for this platform + """ + return self.import_filename - def get_filename(self): - '''Works on all platforms except OSX, which does its own thing.''' - fname = self.get_shbase() - if self.version is None: - return fname - else: - return fname + '.' + self.version - - def get_osx_filename(self): - if self.version is None: - return self.get_shbase() - return self.prefix + self.name + '.' + self.version + '.' + self.suffix - - def set_version(self, version): - if not isinstance(version, str): - raise InvalidArguments('Shared library version is not a string.') - self.version = version + def get_import_filenameslist(self): + if self.import_filename: + return [self.vs_import_filename, self.gcc_import_filename] + return [] - def set_soversion(self, version): - if isinstance(version, int): - version = str(version) - if not isinstance(version, str): - raise InvalidArguments('Shared library soversion is not a string or integer.') - self.soversion = version + def get_all_link_deps(self): + return [self] + self.get_transitive_link_deps() def get_aliaslist(self): - aliases = [] - if self.soversion is not None: - aliases.append(self.get_shbase() + '.' + self.soversion) - if self.version is not None: - aliases.append(self.get_shbase()) + """ + If the versioned library name is libfoo.so.0.100.0, aliases are: + * libfoo.so.0 (soversion) + * libfoo.so (unversioned; for linking) + """ + # Aliases are only useful with .so libraries. Also if the .so library + # ends with .so (no versioning), we don't need aliases. + if self.suffix != 'so' or self.filename.endswith('.so'): + return [] + # Unversioned alias: libfoo.so + aliases = [self.basic_filename_tpl.format(self)] + # If ltversion != soversion we create an soversion alias: libfoo.so.X + if self.ltversion and self.ltversion != self.soversion: + if not self.soversion: + # This is done in self.process_kwargs() + raise AssertionError('BUG: If library version is defined, soversion must have been defined') + alias_tpl = self.filename_tpl.replace('ltversion', 'soversion') + aliases.append(alias_tpl.format(self)) return aliases def type_suffix(self): -- cgit v1.1 From 64cb70441bce61da3258fca93df5baac2eb310ea Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Fri, 1 Jul 2016 14:43:51 +0530 Subject: CustomTarget: Use mesonlib.File objects as-is in the command to be run This allows us to output either the relative or absolute path as requested. Fixes usage of configure_file inside CustomTarget commands with the VS backends. --- mesonbuild/build.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) (limited to 'mesonbuild/build.py') diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 302f824..3ca7bf2 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -989,7 +989,7 @@ class CustomTarget: for i, c in enumerate(cmd): if hasattr(c, 'held_object'): c = c.held_object - if isinstance(c, str): + if isinstance(c, str) or isinstance(c, File): final_cmd.append(c) elif isinstance(c, dependencies.ExternalProgram): if not c.found(): @@ -1005,8 +1005,6 @@ class CustomTarget: if not isinstance(s, str): raise InvalidArguments('Array as argument %d contains a non-string.' % i) final_cmd.append(s) - elif isinstance(c, File): - final_cmd.append(os.path.join(c.subdir, c.fname)) else: raise InvalidArguments('Argument %s in "command" is invalid.' % i) self.command = final_cmd -- cgit v1.1 From 4516e8a49ffff2c8781cce2de9680ed0ce2aab7d Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Fri, 1 Jul 2016 14:43:51 +0530 Subject: Add repr() implementations for build targets and File This aids debugging --- mesonbuild/build.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) (limited to 'mesonbuild/build.py') diff --git a/mesonbuild/build.py b/mesonbuild/build.py index 3ca7bf2..d33f692 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -239,6 +239,10 @@ class BuildTarget(): raise InvalidArguments('Build target %s has no sources.' % name) self.validate_sources() + def __repr__(self): + repr_str = "<{0} {1}: {2}>" + return repr_str.format(self.__class__.__name__, self.get_id(), self.filename) + def get_id(self): # This ID must also be a valid file name on all OSs. # It should also avoid shell metacharacters for obvious @@ -623,6 +627,10 @@ class Generator(): self.exe = exe self.process_kwargs(kwargs) + def __repr__(self): + repr_str = "<{0}: {1}>" + return repr_str.format(self.__class__.__name__, self.exe) + def get_exe(self): return self.exe @@ -953,6 +961,10 @@ class CustomTarget: mlog.log(mlog.bold('Warning:'), 'Unknown keyword arguments in target %s: %s' % (self.name, ', '.join(unknowns))) + def __repr__(self): + repr_str = "<{0} {1}: {2}>" + return repr_str.format(self.__class__.__name__, self.get_id(), self.command) + def get_id(self): return self.name + self.type_suffix() @@ -1079,6 +1091,10 @@ class RunTarget: self.args = args self.subdir = subdir + def __repr__(self): + repr_str = "<{0} {1}: {2}>" + return repr_str.format(self.__class__.__name__, self.get_id(), self.command) + def get_id(self): return self.name + self.type_suffix() @@ -1129,6 +1145,12 @@ class ConfigureFile(): self.targetname = targetname self.configuration_data = configuration_data + def __repr__(self): + repr_str = "<{0}: {1} -> {2}>" + src = os.path.join(self.subdir, self.sourcename) + dst = os.path.join(self.subdir, self.targetname) + return repr_str.format(self.__class__.__name__, src, dst) + def get_configuration_data(self): return self.configuration_data @@ -1146,6 +1168,9 @@ class ConfigurationData(): super().__init__() self.values = {} + def __repr__(self): + return repr(self.values) + def get(self, name): return self.values[name] -- cgit v1.1 From 358598816f69fd05842f30c9f55be21de7603c34 Mon Sep 17 00:00:00 2001 From: Nirbheek Chauhan Date: Mon, 4 Jul 2016 19:49:04 +0530 Subject: build: Fix implementation of sources_are_suffix The first file might be a header file, in which case this test will fail, so check all the files till a match is found instead. Also remove duplicate and incorrect can_compile check. It just checks the suffix and we already check that above. --- mesonbuild/build.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) (limited to 'mesonbuild/build.py') diff --git a/mesonbuild/build.py b/mesonbuild/build.py index d33f692..1ff834c 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -73,7 +73,10 @@ this but it's the best we can do given the way shell quoting works. ''' def sources_are_suffix(sources, suffix): - return len(sources) > 0 and sources[0].endswith('.' + suffix) + for source in sources: + if source.endswith('.' + suffix): + return True + return False def compiler_is_msvc(sources, is_cross, env): """ @@ -96,7 +99,7 @@ def compiler_is_msvc(sources, is_cross, env): compiler = env.detect_cpp_compiler(is_cross) except MesonException: return False - if compiler and compiler.get_id() == 'msvc' and compiler.can_compile(sources[0]): + if compiler and compiler.get_id() == 'msvc': return True return False -- cgit v1.1