diff --git a/docs/markdown/IDE-integration.md b/docs/markdown/IDE-integration.md index 6f0b62916..848b812dc 100644 --- a/docs/markdown/IDE-integration.md +++ b/docs/markdown/IDE-integration.md @@ -32,16 +32,17 @@ to be the last file written. The `meson-info` directory should contain the following files: -| File | Description | -| ------------------------------ | ------------------------------------------------------------------- | -| `intro-benchmarks.json` | Lists all benchmarks | -| `intro-buildoptions.json` | Contains a full list of Meson configuration options for the project | -| `intro-buildsystem_files.json` | Full list of all Meson build files | -| `intro-dependencies.json` | Lists all dependencies used in the project | -| `intro-installed.json` | Contains mapping of files to their installed location | -| `intro-projectinfo.json` | Stores basic information about the project (name, version, etc.) | -| `intro-targets.json` | Full list of all build targets | -| `intro-tests.json` | Lists all tests with instructions how to run them | +| File | Description | +| ------------------------------ | ----------------------------------------------------------------------------- | +| `intro-benchmarks.json` | Lists all benchmarks | +| `intro-buildoptions.json` | Contains a full list of Meson configuration options for the project | +| `intro-buildsystem_files.json` | Full list of all Meson build files | +| `intro-dependencies.json` | Lists all dependencies used in the project | +| `intro-installed.json` | Contains mapping of files to their installed location | +| `intro-install_plan.json` | Dictionary of data types with the source files and their installation details | +| `intro-projectinfo.json` | Stores basic information about the project (name, version, etc.) | +| `intro-targets.json` | Full list of all build targets | +| `intro-tests.json` | Lists all tests with instructions how to run them | The content of the JSON files is further specified in the remainder of this document. @@ -119,6 +120,57 @@ The following table shows all valid types for a target. | `run` | A Meson run target | | `jar` | A Java JAR target | + +### Install plan + +The `intro-install_plan.json` file contains a list of the files that are going +to be installed on the system. + +The data contains a list of files grouped by data type. Each file maps to a +dictionary containing the `destination` and `tag` keys, with the key being the +file location at build time. `destination` is the destination path using +placeholders for the base directories. New keys may be added in the future. + +```json +{ + "targets": { + "/path/to/project/builddir/some_executable": { + "destination": "{bindir}/some_executable", + "tag": "runtime" + }, + "/path/to/project/builddir/libsomelib.so": { + "destination": "{libdir_shared}/libsomelib.so", + "tag": "runtime" + } + }, + "data": { + "/path/to/project/some_data": { + "destination": "{datadir}/some_data", + "tag": null + } + }, + "headers": { + "/path/to/project/some_header.h": { + "destination": "{includedir}/some_header.h", + "tag": "devel" + } + } +} +``` + +Additionally, the `intro-installed.json` file contains the mapping of the +file path at build time to the absolute system location. + +```json +{ + "/path/to/project/builddir/some_executable": "/usr/bin/some_executable", + "/path/to/project/builddir/libsomelib.so": "/user/lib/libsomelib.so", + "/path/to/project/some_data": "/usr/share/some_data", + "/path/to/project/some_header.h": "/usr/include/some_header.h" +} +``` + + ### Using `--targets` without a build directory It is also possible to get most targets without a build directory. diff --git a/mesonbuild/backend/backends.py b/mesonbuild/backend/backends.py index f8ccdbe83..0ebc1f7f3 100644 --- a/mesonbuild/backend/backends.py +++ b/mesonbuild/backend/backends.py @@ -130,12 +130,13 @@ class TargetInstallData: # TODO: install_mode should just always be a FileMode object - def __init__(self, fname: str, outdir: str, aliases: T.Dict[str, str], strip: bool, - install_name_mappings: T.Mapping[str, str], rpath_dirs_to_remove: T.Set[bytes], + def __init__(self, fname: str, outdir: str, outdir_name: str, aliases: T.Dict[str, str], + strip: bool, install_name_mappings: T.Mapping[str, str], rpath_dirs_to_remove: T.Set[bytes], install_rpath: str, install_mode: T.Optional['FileMode'], subproject: str, optional: bool = False, tag: T.Optional[str] = None): self.fname = fname self.outdir = outdir + self.out_name = os.path.join(outdir_name, os.path.basename(fname)) self.aliases = aliases self.strip = strip self.install_name_mappings = install_name_mappings @@ -147,19 +148,22 @@ class TargetInstallData: self.tag = tag class InstallDataBase: - def __init__(self, path: str, install_path: str, install_mode: 'FileMode', - subproject: str, tag: T.Optional[str] = None): + def __init__(self, path: str, install_path: str, install_path_name: str, + install_mode: 'FileMode', subproject: str, tag: T.Optional[str] = None, + data_type: T.Optional[str] = None): self.path = path self.install_path = install_path + self.install_path_name = install_path_name self.install_mode = install_mode self.subproject = subproject self.tag = tag + self.data_type = data_type class SubdirInstallData(InstallDataBase): - def __init__(self, path: str, install_path: str, install_mode: 'FileMode', - exclude: T.Tuple[T.Set[str], T.Set[str]], subproject: str, - tag: T.Optional[str] = None): - super().__init__(path, install_path, install_mode, subproject, tag) + def __init__(self, path: str, install_path: str, install_path_name: str, + install_mode: 'FileMode', exclude: T.Tuple[T.Set[str], T.Set[str]], + subproject: str, tag: T.Optional[str] = None, data_type: T.Optional[str] = None): + super().__init__(path, install_path, install_path_name, install_mode, subproject, tag, data_type) self.exclude = exclude class ExecutableSerialisation: @@ -1139,11 +1143,13 @@ class Backend: return ifilename = os.path.join(self.environment.get_build_dir(), 'depmf.json') ofilename = os.path.join(self.environment.get_prefix(), self.build.dep_manifest_name) + out_name = os.path.join('{prefix}', self.build.dep_manifest_name) mfobj = {'type': 'dependency manifest', 'version': '1.0', 'projects': self.build.dep_manifest} with open(ifilename, 'w', encoding='utf-8') as f: f.write(json.dumps(mfobj)) # Copy file from, to, and with mode unchanged - d.data.append(InstallDataBase(ifilename, ofilename, None, '', tag='devel')) + d.data.append(InstallDataBase(ifilename, ofilename, out_name, None, '', + tag='devel', data_type='depmf')) def get_regen_filelist(self) -> T.List[str]: '''List of all files whose alteration means that the build @@ -1501,7 +1507,7 @@ class Backend: for t in self.build.get_targets().values(): if not t.should_install(): continue - outdirs, custom_install_dir = t.get_install_dir(self.environment) + outdirs, install_dir_name, custom_install_dir = t.get_install_dir(self.environment) # Sanity-check the outputs and install_dirs num_outdirs, num_out = len(outdirs), len(t.get_outputs()) if num_outdirs != 1 and num_outdirs != num_out: @@ -1534,8 +1540,8 @@ class Backend: tag = t.install_tag[0] or ('devel' if isinstance(t, build.StaticLibrary) else 'runtime') mappings = t.get_link_deps_mapping(d.prefix, self.environment) i = TargetInstallData(self.get_target_filename(t), outdirs[0], - t.get_aliases(), should_strip, mappings, - t.rpath_dirs_to_remove, + install_dir_name, t.get_aliases(), + should_strip, mappings, t.rpath_dirs_to_remove, t.install_rpath, install_mode, t.subproject, tag=tag) d.targets.append(i) @@ -1554,7 +1560,8 @@ class Backend: implib_install_dir = self.environment.get_import_lib_dir() # Install the import library; may not exist for shared modules i = TargetInstallData(self.get_target_filename_for_linking(t), - implib_install_dir, {}, False, {}, set(), '', install_mode, + implib_install_dir, install_dir_name, + {}, False, {}, set(), '', install_mode, t.subproject, optional=isinstance(t, build.SharedModule), tag='devel') d.targets.append(i) @@ -1562,6 +1569,7 @@ class Backend: if not should_strip and t.get_debug_filename(): debug_file = os.path.join(self.get_target_dir(t), t.get_debug_filename()) i = TargetInstallData(debug_file, outdirs[0], + install_dir_name, {}, False, {}, set(), '', install_mode, t.subproject, optional=True, tag='devel') @@ -1573,8 +1581,7 @@ class Backend: if outdir is False: continue f = os.path.join(self.get_target_dir(t), output) - tag = tag or self.guess_install_tag(f, outdir) - i = TargetInstallData(f, outdir, {}, False, {}, set(), None, + i = TargetInstallData(f, outdir, install_dir_name, {}, False, {}, set(), None, install_mode, t.subproject, tag=tag) d.targets.append(i) @@ -1589,8 +1596,10 @@ class Backend: if num_outdirs == 1 and num_out > 1: for output, tag in zip(t.get_outputs(), t.install_tag): f = os.path.join(self.get_target_dir(t), output) - tag = tag or self.guess_install_tag(f, outdirs[0]) - i = TargetInstallData(f, outdirs[0], {}, False, {}, set(), None, install_mode, + if not install_dir_name: + dir_name = os.path.join('{prefix}', outdirs[0]) + i = TargetInstallData(f, outdirs[0], dir_name, {}, + False, {}, set(), None, install_mode, t.subproject, optional=not t.build_by_default, tag=tag) d.targets.append(i) @@ -1600,8 +1609,10 @@ class Backend: if outdir is False: continue f = os.path.join(self.get_target_dir(t), output) - tag = tag or self.guess_install_tag(f, outdir) - i = TargetInstallData(f, outdir, {}, False, {}, set(), None, install_mode, + if not install_dir_name: + dir_name = os.path.join('{prefix}', outdir) + i = TargetInstallData(f, outdir, dir_name, + {}, False, {}, set(), None, install_mode, t.subproject, optional=not t.build_by_default, tag=tag) d.targets.append(i) @@ -1616,19 +1627,21 @@ class Backend: srcdir = self.environment.get_source_dir() builddir = self.environment.get_build_dir() for h in headers: - outdir = h.get_custom_install_dir() + outdir = outdir_name = h.get_custom_install_dir() if outdir is None: subdir = h.get_install_subdir() if subdir is None: outdir = incroot + outdir_name = '{includedir}' else: outdir = os.path.join(incroot, subdir) + outdir_name = os.path.join('{includedir}', subdir) for f in h.get_sources(): if not isinstance(f, File): raise MesonException(f'Invalid header type {f!r} can\'t be installed') abspath = f.absolute_path(srcdir, builddir) - i = InstallDataBase(abspath, outdir, h.get_custom_install_mode(), h.subproject, tag='devel') + i = InstallDataBase(abspath, outdir, outdir_name, h.get_custom_install_mode(), h.subproject, tag='devel') d.headers.append(i) def generate_man_install(self, d: InstallData) -> None: @@ -1640,15 +1653,16 @@ class Backend: subdir = m.get_custom_install_dir() if subdir is None: if m.locale: - subdir = os.path.join(manroot, m.locale, 'man' + num) + subdir = os.path.join('{mandir}', m.locale, 'man' + num) else: - subdir = os.path.join(manroot, 'man' + num) + subdir = os.path.join('{mandir}', 'man' + num) fname = f.fname if m.locale: # strip locale from file name fname = fname.replace(f'.{m.locale}', '') srcabs = f.absolute_path(self.environment.get_source_dir(), self.environment.get_build_dir()) - dstabs = os.path.join(subdir, os.path.basename(fname)) - i = InstallDataBase(srcabs, dstabs, m.get_custom_install_mode(), m.subproject, tag='man') + dstname = os.path.join(subdir, os.path.basename(fname)) + dstabs = dstname.replace('{mandir}', manroot) + i = InstallDataBase(srcabs, dstabs, dstname, m.get_custom_install_mode(), m.subproject, tag='man') d.man.append(i) def generate_data_install(self, d: InstallData) -> None: @@ -1658,13 +1672,17 @@ class Backend: for de in data: assert isinstance(de, build.Data) subdir = de.install_dir + subdir_name = de.install_dir_name if not subdir: subdir = os.path.join(self.environment.get_datadir(), self.interpreter.build.project_name) + subdir_name = os.path.join('{datadir}', self.interpreter.build.project_name) for src_file, dst_name in zip(de.sources, de.rename): assert isinstance(src_file, mesonlib.File) dst_abs = os.path.join(subdir, dst_name) + dstdir_name = os.path.join(subdir_name, dst_name) tag = de.install_tag or self.guess_install_tag(dst_abs) - i = InstallDataBase(src_file.absolute_path(srcdir, builddir), dst_abs, de.install_mode, de.subproject, tag=tag) + i = InstallDataBase(src_file.absolute_path(srcdir, builddir), dst_abs, dstdir_name, + de.install_mode, de.subproject, tag=tag, data_type=de.data_type) d.data.append(i) def generate_subdir_install(self, d: InstallData) -> None: @@ -1678,9 +1696,11 @@ class Backend: sd.installable_subdir).rstrip('/') dst_dir = os.path.join(self.environment.get_prefix(), sd.install_dir) + dst_name = os.path.join('{prefix}', sd.install_dir) if not sd.strip_directory: dst_dir = os.path.join(dst_dir, os.path.basename(src_dir)) - i = SubdirInstallData(src_dir, dst_dir, sd.install_mode, sd.exclude, sd.subproject) + dst_name = os.path.join(dst_dir, os.path.basename(src_dir)) + i = SubdirInstallData(src_dir, dst_dir, dst_name, sd.install_mode, sd.exclude, sd.subproject) d.install_subdirs.append(i) def get_introspection_data(self, target_id: str, target: build.Target) -> T.List['TargetIntrospectionData']: @@ -1740,7 +1760,7 @@ class Backend: for t in self.build.get_targets().values(): cross_built = not self.environment.machines.matches_build_machine(t.for_machine) can_run = not cross_built or not self.environment.need_exe_wrapper() - in_default_dir = t.should_install() and not t.get_install_dir(self.environment)[1] + in_default_dir = t.should_install() and not t.get_install_dir(self.environment)[2] if not can_run or not in_default_dir: continue tdir = os.path.join(self.environment.get_build_dir(), self.get_target_dir(t)) diff --git a/mesonbuild/build.py b/mesonbuild/build.py index b0863cd61..b5fa8ea37 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -519,12 +519,12 @@ class Target(HoldableObject): return NotImplemented return self.get_id() >= other.get_id() - def get_default_install_dir(self, env: environment.Environment) -> str: + def get_default_install_dir(self, env: environment.Environment) -> T.Tuple[str, str]: raise NotImplementedError - def get_install_dir(self, environment: environment.Environment) -> T.Tuple[T.Any, bool]: + def get_install_dir(self, environment: environment.Environment) -> T.Tuple[T.Any, str, bool]: # Find the installation directory. - default_install_dir = self.get_default_install_dir(environment) + default_install_dir, install_dir_name = self.get_default_install_dir(environment) outdirs = self.get_custom_install_dir() if outdirs[0] is not None and outdirs[0] != default_install_dir and outdirs[0] is not True: # Either the value is set to a non-default value, or is set to @@ -534,7 +534,7 @@ class Target(HoldableObject): else: custom_install_dir = False outdirs[0] = default_install_dir - return outdirs, custom_install_dir + return outdirs, install_dir_name, custom_install_dir def get_basename(self) -> str: return self.name @@ -942,8 +942,8 @@ class BuildTarget(Target): result.update(i.get_link_dep_subdirs()) return result - def get_default_install_dir(self, environment: environment.Environment) -> str: - return environment.get_libdir() + def get_default_install_dir(self, environment: environment.Environment) -> T.Tuple[str, str]: + return environment.get_libdir(), '{libdir}' def get_custom_install_dir(self): return self.install_dir @@ -1729,8 +1729,8 @@ class Executable(BuildTarget): # Remember that this exe was returned by `find_program()` through an override self.was_returned_by_find_program = False - def get_default_install_dir(self, environment: environment.Environment) -> str: - return environment.get_bindir() + def get_default_install_dir(self, environment: environment.Environment) -> T.Tuple[str, str]: + return environment.get_bindir(), '{bindir}' def description(self): '''Human friendly description of the executable''' @@ -1806,8 +1806,8 @@ class StaticLibrary(BuildTarget): def get_link_deps_mapping(self, prefix: str, environment: environment.Environment) -> T.Mapping[str, str]: return {} - def get_default_install_dir(self, environment): - return environment.get_static_lib_dir() + def get_default_install_dir(self, environment) -> T.Tuple[str, str]: + return environment.get_static_lib_dir(), '{libdir_static}' def type_suffix(self): return "@sta" @@ -1866,14 +1866,14 @@ class SharedLibrary(BuildTarget): old = get_target_macos_dylib_install_name(self) if old not in mappings: fname = self.get_filename() - outdirs, _ = self.get_install_dir(self.environment) + outdirs, _, _ = self.get_install_dir(self.environment) new = os.path.join(prefix, outdirs[0], fname) result.update({old: new}) mappings.update(result) return mappings - def get_default_install_dir(self, environment): - return environment.get_shared_lib_dir() + def get_default_install_dir(self, environment) -> T.Tuple[str, str]: + return environment.get_shared_lib_dir(), '{libdir_shared}' def determine_filenames(self, env): """ @@ -2160,8 +2160,8 @@ class SharedModule(SharedLibrary): super().__init__(name, subdir, subproject, for_machine, sources, objects, environment, kwargs) self.typename = 'shared module' - def get_default_install_dir(self, environment): - return environment.get_shared_module_dir() + def get_default_install_dir(self, environment) -> T.Tuple[str, str]: + return environment.get_shared_module_dir(), '{moduledir_shared}' class BothLibraries(SecondLevelHolder): def __init__(self, shared: SharedLibrary, static: StaticLibrary) -> None: @@ -2253,8 +2253,8 @@ class CustomTarget(Target, CommandBase): if unknowns: mlog.warning('Unknown keyword arguments in target {}: {}'.format(self.name, ', '.join(unknowns))) - def get_default_install_dir(self, environment): - return None + def get_default_install_dir(self, environment) -> T.Tuple[str, str]: + return None, None def __repr__(self): repr_str = "<{0} {1}: {2}>" @@ -2656,12 +2656,14 @@ class ConfigurationData(HoldableObject): # A bit poorly named, but this represents plain data files to copy # during install. class Data(HoldableObject): - def __init__(self, sources: T.List[File], install_dir: str, + def __init__(self, sources: T.List[File], install_dir: str, install_dir_name: str, install_mode: 'FileMode', subproject: str, rename: T.List[str] = None, - install_tag: T.Optional[str] = None): + install_tag: T.Optional[str] = None, + data_type: str = None): self.sources = sources self.install_dir = install_dir + self.install_dir_name = install_dir_name self.install_mode = install_mode self.install_tag = install_tag if rename is None: @@ -2669,6 +2671,7 @@ class Data(HoldableObject): else: self.rename = rename self.subproject = subproject + self.data_type = data_type class TestSetup: def __init__(self, exe_wrapper: T.Optional[T.List[str]], gdb: bool, diff --git a/mesonbuild/interpreter/interpreter.py b/mesonbuild/interpreter/interpreter.py index fe27cb3cd..891c606dd 100644 --- a/mesonbuild/interpreter/interpreter.py +++ b/mesonbuild/interpreter/interpreter.py @@ -1926,14 +1926,24 @@ This will become a hard error in the future.''' % kwargs['input'], location=self '"rename" and "sources" argument lists must be the same length if "rename" is given. ' f'Rename has {len(rename)} elements and sources has {len(sources)}.') - return self.install_data_impl(sources, kwargs['install_dir'], kwargs['install_mode'], rename, - kwargs['install_tag']) + install_dir_name = kwargs['install_dir'] + if install_dir_name: + if not os.path.isabs(install_dir_name): + install_dir_name = os.path.join('{datadir}', install_dir_name) + else: + install_dir_name = '{datadir}' + return self.install_data_impl(sources, kwargs['install_dir'], kwargs['install_mode'], + rename, kwargs['install_tag'], install_dir_name) def install_data_impl(self, sources: T.List[mesonlib.File], install_dir: str, install_mode: FileMode, rename: T.Optional[str], - tag: T.Optional[str]) -> build.Data: + tag: T.Optional[str], + install_dir_name: T.Optional[str] = None, + install_data_type: T.Optional[str] = None) -> build.Data: + """Just the implementation with no validation.""" - data = build.Data(sources, install_dir, install_mode, self.subproject, rename, tag) + data = build.Data(sources, install_dir, install_dir_name or install_dir, install_mode, + self.subproject, rename, tag, install_data_type) self.build.data.append(data) return data @@ -2158,7 +2168,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self install_tag = kwargs.get('install_tag') if install_tag is not None and not isinstance(install_tag, str): raise InvalidArguments('install_tag keyword argument must be string') - self.build.data.append(build.Data([cfile], idir, install_mode, self.subproject, install_tag=install_tag)) + self.build.data.append(build.Data([cfile], idir, idir, install_mode, self.subproject, + install_tag=install_tag, data_type='configure')) return mesonlib.File.from_built_file(self.subdir, output) def extract_incdirs(self, kwargs): diff --git a/mesonbuild/mintro.py b/mesonbuild/mintro.py index 73790a5ae..ab668d530 100644 --- a/mesonbuild/mintro.py +++ b/mesonbuild/mintro.py @@ -74,6 +74,7 @@ def get_meson_introspection_types(coredata: T.Optional[cdata.CoreData] = None, ('dependencies', IntroCommand('List external dependencies', func=lambda: list_deps(coredata), no_bd=list_deps_from_source)), ('scan_dependencies', IntroCommand('Scan for dependencies used in the meson.build file', no_bd=list_deps_from_source)), ('installed', IntroCommand('List all installed files and directories', func=lambda: list_installed(installdata))), + ('install_plan', IntroCommand('List all installed files and directories with their details', func=lambda: list_install_plan(installdata))), ('projectinfo', IntroCommand('Information about projects', func=lambda: list_projinfo(builddata), no_bd=list_projinfo_from_source)), ('targets', IntroCommand('List top level targets', func=lambda: list_targets(builddata, installdata, backend), no_bd=list_targets_from_source)), ('tests', IntroCommand('List all unit tests', func=lambda: list_tests(testdata))), @@ -119,6 +120,36 @@ def list_installed(installdata: backends.InstallData) -> T.Dict[str, str]: res[i.path] = os.path.join(installdata.prefix, i.install_path) return res +def list_install_plan(installdata: backends.InstallData) -> T.Dict[str, T.Dict[str, T.Dict[str, T.Optional[str]]]]: + plan = { + 'targets': { + os.path.join(installdata.build_dir, target.fname): { + 'destination': target.out_name, + 'tag': target.tag or None, + } + for target in installdata.targets + }, + } # type: T.Dict[str, T.Dict[str, T.Dict[str, T.Optional[str]]]] + for key, data_list in { + 'data': installdata.data, + 'man': installdata.man, + 'headers': installdata.headers, + }.items(): + for data in data_list: + data_type = data.data_type or key + install_path_name = data.install_path_name + if key == 'headers': # in the headers, install_path_name is the directory + install_path_name = os.path.join(install_path_name, os.path.basename(data.path)) + elif data_type == 'configure': + install_path_name = os.path.join('{prefix}', install_path_name) + + plan[data_type] = plan.get(data_type, {}) + plan[data_type][data.path] = { + 'destination': install_path_name, + 'tag': data.tag or None, + } + return plan + def get_target_dir(coredata: cdata.CoreData, subdir: str) -> str: if coredata.get_option(OptionKey('layout')) == 'flat': return 'meson-out' diff --git a/mesonbuild/modules/cmake.py b/mesonbuild/modules/cmake.py index d0b60651f..53beded40 100644 --- a/mesonbuild/modules/cmake.py +++ b/mesonbuild/modules/cmake.py @@ -272,9 +272,10 @@ class CmakeModule(ExtensionModule): if not self.detect_cmake(): raise mesonlib.MesonException('Unable to find cmake') - pkgroot = kwargs.get('install_dir', None) + pkgroot = pkgroot_name = kwargs.get('install_dir', None) if pkgroot is None: pkgroot = os.path.join(state.environment.coredata.get_option(mesonlib.OptionKey('libdir')), 'cmake', name) + pkgroot_name = os.path.join('{libdir}', 'cmake', name) if not isinstance(pkgroot, str): raise mesonlib.MesonException('Install_dir must be a string.') @@ -290,7 +291,7 @@ class CmakeModule(ExtensionModule): } mesonlib.do_conf_file(template_file, version_file, conf, 'meson') - res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), version_file)], pkgroot, None, state.subproject) + res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), version_file)], pkgroot, pkgroot_name, None, state.subproject) return ModuleReturnValue(res, [res]) def create_package_file(self, infile, outfile, PACKAGE_RELATIVE_PATH, extra, confdata): @@ -375,7 +376,7 @@ class CmakeModule(ExtensionModule): if conffile not in self.interpreter.build_def_files: self.interpreter.build_def_files.append(conffile) - res = build.Data([mesonlib.File(True, ofile_path, ofile_fname)], install_dir, None, state.subproject) + res = build.Data([mesonlib.File(True, ofile_path, ofile_fname)], install_dir, install_dir, None, state.subproject) self.interpreter.build.data.append(res) return res diff --git a/mesonbuild/modules/gnome.py b/mesonbuild/modules/gnome.py index 5515919f9..1343bc739 100644 --- a/mesonbuild/modules/gnome.py +++ b/mesonbuild/modules/gnome.py @@ -1728,7 +1728,7 @@ G_END_DECLS''' with open(fname, 'w', encoding='utf-8') as ofile: for package in packages: ofile.write(package + '\n') - return build.Data([mesonlib.File(True, outdir, fname)], install_dir, None, state.subproject) + return build.Data([mesonlib.File(True, outdir, fname)], install_dir, install_dir, None, state.subproject) def _get_vapi_link_with(self, target): link_with = [] diff --git a/mesonbuild/modules/pkgconfig.py b/mesonbuild/modules/pkgconfig.py index e57c4f665..f73e16882 100644 --- a/mesonbuild/modules/pkgconfig.py +++ b/mesonbuild/modules/pkgconfig.py @@ -537,18 +537,20 @@ class PkgConfigModule(ExtensionModule): unescaped_variables = parse_variable_list(unescaped_variables) pcfile = filebase + '.pc' - pkgroot = kwargs.get('install_dir', default_install_dir) + pkgroot = pkgroot_name = kwargs.get('install_dir', default_install_dir) if pkgroot is None: if mesonlib.is_freebsd(): pkgroot = os.path.join(state.environment.coredata.get_option(mesonlib.OptionKey('prefix')), 'libdata', 'pkgconfig') + pkgroot_name = os.path.join('{prefix}', 'libdata', 'pkgconfig') else: pkgroot = os.path.join(state.environment.coredata.get_option(mesonlib.OptionKey('libdir')), 'pkgconfig') + pkgroot_name = os.path.join('{libdir}', 'pkgconfig') if not isinstance(pkgroot, str): raise mesonlib.MesonException('Install_dir must be a string.') self._generate_pkgconfig_file(state, deps, subdirs, name, description, url, version, pcfile, conflicts, variables, unescaped_variables, False, dataonly) - res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), pcfile)], pkgroot, None, state.subproject, install_tag='devel') + res = build.Data([mesonlib.File(True, state.environment.get_scratch_dir(), pcfile)], pkgroot, pkgroot_name, None, state.subproject, install_tag='devel') variables = self.interpreter.extract_variables(kwargs, argname='uninstalled_variables', dict_new=True) variables = parse_variable_list(variables) unescaped_variables = self.interpreter.extract_variables(kwargs, argname='unescaped_uninstalled_variables') diff --git a/mesonbuild/modules/python.py b/mesonbuild/modules/python.py index bddaa192c..57cea91b5 100644 --- a/mesonbuild/modules/python.py +++ b/mesonbuild/modules/python.py @@ -493,7 +493,8 @@ class PythonInstallation(ExternalProgramHolder): return self.interpreter.install_data_impl( self.interpreter.source_strings_to_files(args[0]), self._get_install_dir_impl(kwargs['pure'], kwargs['subdir']), - mesonlib.FileMode(), rename=None, tag=tag) + mesonlib.FileMode(), rename=None, tag=tag, install_data_type='python', + install_dir_name=self._get_install_dir_name_impl(kwargs['pure'], kwargs['subdir'])) @noPosargs @typed_kwargs('python_installation.install_dir', _PURE_KW, _SUBDIR_KW) @@ -504,6 +505,9 @@ class PythonInstallation(ExternalProgramHolder): return os.path.join( self.purelib_install_path if pure else self.platlib_install_path, subdir) + def _get_install_dir_name_impl(self, pure: bool, subdir: str) -> str: + return os.path.join('{py_purelib}' if pure else '{py_platlib}', subdir) + @noPosargs @noKwargs def language_version_method(self, args: T.List['TYPE_var'], kwargs: 'TYPE_kwargs') -> str: diff --git a/test cases/unit/98 install tag/bar-custom.txt b/test cases/unit/98 install all targets/bar-custom.txt similarity index 100% rename from test cases/unit/98 install tag/bar-custom.txt rename to test cases/unit/98 install all targets/bar-custom.txt diff --git a/test cases/unit/98 install tag/bar-devel.h b/test cases/unit/98 install all targets/bar-devel.h similarity index 100% rename from test cases/unit/98 install tag/bar-devel.h rename to test cases/unit/98 install all targets/bar-devel.h diff --git a/test cases/unit/98 install tag/bar-notag.txt b/test cases/unit/98 install all targets/bar-notag.txt similarity index 100% rename from test cases/unit/98 install tag/bar-notag.txt rename to test cases/unit/98 install all targets/bar-notag.txt diff --git a/test cases/unit/98 install tag/foo.in b/test cases/unit/98 install all targets/foo.in similarity index 100% rename from test cases/unit/98 install tag/foo.in rename to test cases/unit/98 install all targets/foo.in diff --git a/test cases/unit/98 install tag/foo1-devel.h b/test cases/unit/98 install all targets/foo1-devel.h similarity index 100% rename from test cases/unit/98 install tag/foo1-devel.h rename to test cases/unit/98 install all targets/foo1-devel.h diff --git a/test cases/unit/98 install tag/lib.c b/test cases/unit/98 install all targets/lib.c similarity index 100% rename from test cases/unit/98 install tag/lib.c rename to test cases/unit/98 install all targets/lib.c diff --git a/test cases/unit/98 install tag/main.c b/test cases/unit/98 install all targets/main.c similarity index 100% rename from test cases/unit/98 install tag/main.c rename to test cases/unit/98 install all targets/main.c diff --git a/test cases/unit/98 install tag/meson.build b/test cases/unit/98 install all targets/meson.build similarity index 99% rename from test cases/unit/98 install tag/meson.build rename to test cases/unit/98 install all targets/meson.build index ad1692ae1..d186eb7a4 100644 --- a/test cases/unit/98 install tag/meson.build +++ b/test cases/unit/98 install all targets/meson.build @@ -1,5 +1,7 @@ project('install tag', 'c') +subdir('subdir') + # Those files should not be tagged configure_file(input: 'foo.in', output: 'foo-notag.h', configuration: {'foo': 'bar'}, diff --git a/test cases/unit/98 install tag/script.py b/test cases/unit/98 install all targets/script.py similarity index 100% rename from test cases/unit/98 install tag/script.py rename to test cases/unit/98 install all targets/script.py diff --git a/test cases/unit/98 install all targets/subdir/bar2-devel.h b/test cases/unit/98 install all targets/subdir/bar2-devel.h new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/unit/98 install all targets/subdir/foo2.in b/test cases/unit/98 install all targets/subdir/foo2.in new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/unit/98 install all targets/subdir/foo3-devel.h b/test cases/unit/98 install all targets/subdir/foo3-devel.h new file mode 100644 index 000000000..e69de29bb diff --git a/test cases/unit/98 install all targets/subdir/lib.c b/test cases/unit/98 install all targets/subdir/lib.c new file mode 100644 index 000000000..2ea9c7dd1 --- /dev/null +++ b/test cases/unit/98 install all targets/subdir/lib.c @@ -0,0 +1,9 @@ +#if defined _WIN32 || defined __CYGWIN__ +#define DLL_PUBLIC __declspec(dllexport) +#else +#define DLL_PUBLIC +#endif + +int DLL_PUBLIC foo(void) { + return 0; +} diff --git a/test cases/unit/98 install all targets/subdir/main.c b/test cases/unit/98 install all targets/subdir/main.c new file mode 100644 index 000000000..0fb4389f7 --- /dev/null +++ b/test cases/unit/98 install all targets/subdir/main.c @@ -0,0 +1,3 @@ +int main(int argc, char *argv[]) { + return 0; +} diff --git a/test cases/unit/98 install all targets/subdir/meson.build b/test cases/unit/98 install all targets/subdir/meson.build new file mode 100644 index 000000000..53c796abe --- /dev/null +++ b/test cases/unit/98 install all targets/subdir/meson.build @@ -0,0 +1,21 @@ +configure_file(input: 'foo2.in', output: 'foo2.h', + configuration: {'foo': 'bar'}, + install_dir: get_option('datadir'), + install: true, +) +custom_target('ct4', + output: ['out1.txt', 'out2.txt'], + command: ['script.py', '@OUTPUT@'], + install_dir: get_option('datadir'), + install: true, +) +install_headers('foo3-devel.h') +install_data('bar2-devel.h', + install_dir: get_option('includedir'), +) +executable('app2', 'main.c', + install: true, +) +both_libraries('both2', 'lib.c', + install: true, +) diff --git a/test cases/unit/98 install all targets/subdir/script.py b/test cases/unit/98 install all targets/subdir/script.py new file mode 100644 index 000000000..c5f3be9ed --- /dev/null +++ b/test cases/unit/98 install all targets/subdir/script.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +import sys + +for f in sys.argv[1:]: + with open(f, 'w') as f: + pass diff --git a/unittests/allplatformstests.py b/unittests/allplatformstests.py index bf90bed69..6ead45ed9 100644 --- a/unittests/allplatformstests.py +++ b/unittests/allplatformstests.py @@ -52,7 +52,7 @@ from mesonbuild.compilers import ( ) from mesonbuild.dependencies import PkgConfigDependency -from mesonbuild.build import Target, ConfigurationData +from mesonbuild.build import Target, ConfigurationData, Executable, SharedLibrary, StaticLibrary import mesonbuild.modules.pkgconfig from mesonbuild.scripts import destdir_join @@ -2837,6 +2837,7 @@ class AllPlatformTests(BasePlatformTests): 'buildsystem_files', 'dependencies', 'installed', + 'install_plan', 'projectinfo', 'targets', 'tests', @@ -3772,7 +3773,7 @@ class AllPlatformTests(BasePlatformTests): self.assertEqual(sorted(link_args), sorted(['-flto'])) def test_install_tag(self) -> None: - testdir = os.path.join(self.unit_test_dir, '98 install tag') + testdir = os.path.join(self.unit_test_dir, '98 install all targets') self.init(testdir) self.build() @@ -3804,29 +3805,36 @@ class AllPlatformTests(BasePlatformTests): expected_devel = expected_common | { Path(installpath, 'usr/include'), - Path(installpath, 'usr/include/foo1-devel.h'), Path(installpath, 'usr/include/bar-devel.h'), + Path(installpath, 'usr/include/bar2-devel.h'), + Path(installpath, 'usr/include/foo1-devel.h'), Path(installpath, 'usr/include/foo2-devel.h'), + Path(installpath, 'usr/include/foo3-devel.h'), Path(installpath, 'usr/include/out-devel.h'), Path(installpath, 'usr/lib'), Path(installpath, 'usr/lib/libstatic.a'), Path(installpath, 'usr/lib/libboth.a'), + Path(installpath, 'usr/lib/libboth2.a'), } if cc.get_id() in {'msvc', 'clang-cl'}: expected_devel |= { Path(installpath, 'usr/bin'), Path(installpath, 'usr/bin/app.pdb'), + Path(installpath, 'usr/bin/app2.pdb'), Path(installpath, 'usr/bin/both.pdb'), + Path(installpath, 'usr/bin/both2.pdb'), Path(installpath, 'usr/bin/bothcustom.pdb'), Path(installpath, 'usr/bin/shared.pdb'), Path(installpath, 'usr/lib/both.lib'), + Path(installpath, 'usr/lib/both2.lib'), Path(installpath, 'usr/lib/bothcustom.lib'), Path(installpath, 'usr/lib/shared.lib'), } elif is_windows() or is_cygwin(): expected_devel |= { Path(installpath, 'usr/lib/libboth.dll.a'), + Path(installpath, 'usr/lib/libboth2.dll.a'), Path(installpath, 'usr/lib/libshared.dll.a'), Path(installpath, 'usr/lib/libbothcustom.dll.a'), } @@ -3834,8 +3842,10 @@ class AllPlatformTests(BasePlatformTests): expected_runtime = expected_common | { Path(installpath, 'usr/bin'), Path(installpath, 'usr/bin/' + exe_name('app')), + Path(installpath, 'usr/bin/' + exe_name('app2')), Path(installpath, 'usr/' + shared_lib_name('shared')), Path(installpath, 'usr/' + shared_lib_name('both')), + Path(installpath, 'usr/' + shared_lib_name('both2')), } expected_custom = expected_common | { @@ -3863,6 +3873,9 @@ class AllPlatformTests(BasePlatformTests): Path(installpath, 'usr/share/out1-notag.txt'), Path(installpath, 'usr/share/out2-notag.txt'), Path(installpath, 'usr/share/out3-notag.txt'), + Path(installpath, 'usr/share/foo2.h'), + Path(installpath, 'usr/share/out1.txt'), + Path(installpath, 'usr/share/out2.txt'), } def do_install(tags=None): @@ -3876,3 +3889,166 @@ class AllPlatformTests(BasePlatformTests): self.assertEqual(sorted(expected_custom), do_install('custom')) self.assertEqual(sorted(expected_runtime_custom), do_install('runtime,custom')) self.assertEqual(sorted(expected_all), do_install()) + + def test_introspect_install_plan(self): + testdir = os.path.join(self.unit_test_dir, '98 install all targets') + introfile = os.path.join(self.builddir, 'meson-info', 'intro-install_plan.json') + self.init(testdir) + self.assertPathExists(introfile) + with open(introfile, encoding='utf-8') as fp: + res = json.load(fp) + + env = get_fake_env(testdir, self.builddir, self.prefix) + + def output_name(name, type_): + return type_(name=name, subdir=None, subproject=None, + for_machine=MachineChoice.HOST, sources=[], + objects=[], environment=env, kwargs={}).filename + + shared_lib_name = lambda name: output_name(name, SharedLibrary) + static_lib_name = lambda name: output_name(name, StaticLibrary) + exe_name = lambda name: output_name(name, Executable) + + expected = { + 'targets': { + f'{self.builddir}/out1-notag.txt': { + 'destination': '{prefix}/share/out1-notag.txt', + 'tag': None, + }, + f'{self.builddir}/out2-notag.txt': { + 'destination': '{prefix}/share/out2-notag.txt', + 'tag': None, + }, + f'{self.builddir}/libstatic.a': { + 'destination': '{libdir_static}/libstatic.a', + 'tag': 'devel', + }, + f'{self.builddir}/' + exe_name('app'): { + 'destination': '{bindir}/' + exe_name('app'), + 'tag': 'runtime', + }, + f'{self.builddir}/subdir/' + exe_name('app2'): { + 'destination': '{bindir}/' + exe_name('app2'), + 'tag': 'runtime', + }, + f'{self.builddir}/' + shared_lib_name('shared'): { + 'destination': '{libdir_shared}/' + shared_lib_name('shared'), + 'tag': 'runtime', + }, + f'{self.builddir}/' + shared_lib_name('both'): { + 'destination': '{libdir_shared}/' + shared_lib_name('both'), + 'tag': 'runtime', + }, + f'{self.builddir}/' + static_lib_name('both'): { + 'destination': '{libdir_static}/' + static_lib_name('both'), + 'tag': 'devel', + }, + f'{self.builddir}/' + shared_lib_name('bothcustom'): { + 'destination': '{libdir_shared}/' + shared_lib_name('bothcustom'), + 'tag': 'custom', + }, + f'{self.builddir}/' + static_lib_name('bothcustom'): { + 'destination': '{libdir_static}/' + static_lib_name('bothcustom'), + 'tag': 'custom', + }, + f'{self.builddir}/subdir/' + shared_lib_name('both2'): { + 'destination': '{libdir_shared}/' + shared_lib_name('both2'), + 'tag': 'runtime', + }, + f'{self.builddir}/subdir/' + static_lib_name('both2'): { + 'destination': '{libdir_static}/' + static_lib_name('both2'), + 'tag': 'devel', + }, + f'{self.builddir}/out1-custom.txt': { + 'destination': '{prefix}/share/out1-custom.txt', + 'tag': 'custom', + }, + f'{self.builddir}/out2-custom.txt': { + 'destination': '{prefix}/share/out2-custom.txt', + 'tag': 'custom', + }, + f'{self.builddir}/out3-custom.txt': { + 'destination': '{prefix}/share/out3-custom.txt', + 'tag': 'custom', + }, + f'{self.builddir}/subdir/out1.txt': { + 'destination': '{prefix}/share/out1.txt', + 'tag': None, + }, + f'{self.builddir}/subdir/out2.txt': { + 'destination': '{prefix}/share/out2.txt', + 'tag': None, + }, + f'{self.builddir}/out-devel.h': { + 'destination': '{prefix}/include/out-devel.h', + 'tag': 'devel', + }, + f'{self.builddir}/out3-notag.txt': { + 'destination': '{prefix}/share/out3-notag.txt', + 'tag': None, + }, + }, + 'configure': { + f'{self.builddir}/foo-notag.h': { + 'destination': '{prefix}/share/foo-notag.h', + 'tag': None, + }, + f'{self.builddir}/foo2-devel.h': { + 'destination': '{prefix}/include/foo2-devel.h', + 'tag': 'devel', + }, + f'{self.builddir}/foo-custom.h': { + 'destination': '{prefix}/share/foo-custom.h', + 'tag': 'custom', + }, + f'{self.builddir}/subdir/foo2.h': { + 'destination': '{prefix}/share/foo2.h', + 'tag': None, + }, + }, + 'data': { + f'{testdir}/bar-notag.txt': { + 'destination': '{datadir}/share/bar-notag.txt', + 'tag': None, + }, + f'{testdir}/bar-devel.h': { + 'destination': '{datadir}/include/bar-devel.h', + 'tag': 'devel', + }, + f'{testdir}/bar-custom.txt': { + 'destination': '{datadir}/share/bar-custom.txt', + 'tag': 'custom', + }, + f'{testdir}/subdir/bar2-devel.h': { + 'destination': '{datadir}/include/bar2-devel.h', + 'tag': 'devel', + }, + }, + 'headers': { + f'{testdir}/foo1-devel.h': { + 'destination': '{includedir}/foo1-devel.h', + 'tag': 'devel', + }, + f'{testdir}/subdir/foo3-devel.h': { + 'destination': '{includedir}/foo3-devel.h', + 'tag': 'devel', + }, + } + } + + fix_path = lambda path: os.path.sep.join(path.split('/')) + expected_fixed = { + data_type: { + fix_path(source): { + key: fix_path(value) if key == 'destination' else value + for key, value in attributes.items() + } + for source, attributes in files.items() + } + for data_type, files in expected.items() + } + + for data_type, files in expected_fixed.items(): + for file, details in files.items(): + with self.subTest(key='{}.{}'.format(data_type, file)): + self.assertEqual(res[data_type][file], details)