diff --git a/contrib/macdeploy/README.md b/contrib/macdeploy/README.md index ea143c17d..2277cfc89 100644 --- a/contrib/macdeploy/README.md +++ b/contrib/macdeploy/README.md @@ -1,127 +1,127 @@ # MacOS Deployment -The `macdeployqtplus` script should not be run manually. Instead, after building as usual: +The `macdeployqtplus.py` script should not be run manually. Instead, after building as usual: ```bash ninja osx-dmg ``` During the deployment process, the disk image window will pop up briefly when the fancy settings are applied. This is normal, please do not interfere, the process will unmount the DMG and cleanup before finishing. When complete, it will have produced `Bitcoin-ABC.dmg`. ## SDK Extraction ### Step 1: Obtaining `Xcode.app` Our current macOS SDK (`Xcode-11.3.1-11C505-extracted-SDK-with-libcxx-headers.tar.gz`) can be extracted from [Xcode_11.3.1.xip](https://download.developer.apple.com/Developer_Tools/Xcode_11.3.1/Xcode_11.3.1.xip). An Apple ID is needed to download this. After Xcode version 7.x, Apple started shipping the `Xcode.app` in a `.xip` archive. This makes the SDK less-trivial to extract on non-macOS machines. One approach (tested on Debian Buster) is outlined below: ```bash # Install/clone tools needed for extracting Xcode.app apt install cpio # Unpack Xcode_11.3.1.xip and place the resulting Xcode.app in your current # working directory python3 contrib/apple-sdk-tools/extract_xcode.py -f Xcode_11.3.1.xip | cpio -d -i ``` On macOS the process is more straightforward: ```bash xip -x Xcode_11.3.1.xip ``` ### Step 2: Generating `Xcode-11.3.1-11C505-extracted-SDK-with-libcxx-headers.tar.gz` from `Xcode.app` To generate `Xcode-11.3.1-11C505-extracted-SDK-with-libcxx-headers.tar.gz`, run the script [`gen-sdk`](./gen-sdk) with the path to `Xcode.app` (extracted in the previous stage) as the first argument. ```bash # Generate a Xcode-11.3.1-11C505-extracted-SDK-with-libcxx-headers.tar.gz from # the supplied Xcode.app ./contrib/macdeploy/gen-sdk '/path/to/Xcode.app' ``` ## Deterministic macOS DMG Notes Working macOS DMGs are created in Linux by combining a recent `clang`, the Apple `binutils` (`ld`, `ar`, etc) and DMG authoring tools. Apple uses `clang` extensively for development and has upstreamed the necessary functionality so that a vanilla clang can take advantage. It supports the use of `-F`, `-target`, `-mmacosx-version-min`, and `--sysroot`, which are all necessary when building for macOS. Apple's version of `binutils` (called `cctools`) contains lots of functionality missing in the FSF's `binutils`. In addition to extra linker options for frameworks and sysroots, several other tools are needed as well such as `install_name_tool`, `lipo`, and `nmedit`. These do not build under Linux, so they have been patched to do so. The work here was used as a starting point: [mingwandroid/toolchain4](https://github.com/mingwandroid/toolchain4). In order to build a working toolchain, the following source packages are needed from Apple: `cctools`, `dyld`, and `ld64`. These tools inject timestamps by default, which produce non-deterministic binaries. The `ZERO_AR_DATE` environment variable is used to disable that. This version of `cctools` has been patched to use the current version of `clang`'s headers and its `libLTO.so` rather than those from `llvmgcc`, as it was originally done in `toolchain4`. To complicate things further, all builds must target an Apple SDK. These SDKs are free to download, but not redistributable. To obtain it, register for an Apple Developer Account, then download [Xcode_11.3.1](https://download.developer.apple.com/Developer_Tools/Xcode_11.3.1/Xcode_11.3.1.xip). This file is many gigabytes in size, but most (but not all) of what we need is contained only in a single directory: ```bash Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk ``` See the SDK Extraction notes above for how to obtain it. The Gitian descriptors build 2 sets of files: Linux tools, then Apple binaries which are created using these tools. The build process has been designed to avoid including the SDK's files in Gitian's outputs. All interim tarballs are fully deterministic and may be freely redistributed. `genisoimage` is used to create the initial DMG. It is not deterministic as-is, so it has been patched. A system `genisoimage` will work fine, but it will not be deterministic because the file-order will change between invocations. The patch can be seen here: [cdrkit-deterministic.patch](https://github.com/bitcoin/bitcoin/blob/master/depends/patches/native_cdrkit/cdrkit-deterministic.patch). No effort was made to fix this cleanly, so it likely leaks memory badly, however it's only used for a single invocation, so that's no real concern. `genisoimage` cannot compress DMGs, so afterwards, the DMG tool from the `libdmg-hfsplus` project is used to compress it. There are several bugs in this tool and its maintainer has seemingly abandoned the project. The DMG tool has the ability to create DMGs from scratch as well, but this functionality is broken. Only the compression feature is currently used. Ideally, the creation could be fixed and `genisoimage` would no longer be necessary. Background images and other features can be added to DMG files by inserting a `.DS_Store` before creation. This is generated by the script `contrib/macdeploy/custom_dsstore.py`. As of OS X 10.9 Mavericks, using an Apple-blessed key to sign binaries is a requirement in order to satisfy the new Gatekeeper requirements. Because this private key cannot be shared, we'll have to be a bit creative in order for the build process to remain somewhat deterministic. Here's how it works: - Builders use Gitian to create an unsigned release. This outputs an unsigned DMG which users may choose to bless and run. It also outputs an unsigned app structure in the form of a tarball, which also contains all of the tools that have been previously (deterministically) built in order to create a final DMG. - The Apple keyholder uses this unsigned app to create a detached signature, using the script that is also included there. Detached signatures are available from this [repository](https://github.com/bitcoin-core/bitcoin-detached-sigs). - Builders feed the unsigned app + detached signature back into Gitian. It uses the pre-built tools to recombine the pieces into a deterministic DMG. diff --git a/contrib/macdeploy/macdeployqtplus b/contrib/macdeploy/macdeployqtplus.py similarity index 68% rename from contrib/macdeploy/macdeployqtplus rename to contrib/macdeploy/macdeployqtplus.py index 9e828feee..3ddc6c6c9 100755 --- a/contrib/macdeploy/macdeployqtplus +++ b/contrib/macdeploy/macdeployqtplus.py @@ -1,908 +1,1135 @@ #!/usr/bin/env python3 # # Copyright (C) 2011 Patrick "p2k" Schneider # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . # -import subprocess, sys, re, os, shutil, stat, os.path, time +import subprocess +import sys +import re +import os +import shutil +import stat +import os.path +import time from string import Template from argparse import ArgumentParser from typing import List, Optional # This is ported from the original macdeployqt with modifications + class FrameworkInfo(object): def __init__(self): self.frameworkDirectory = "" self.frameworkName = "" self.frameworkPath = "" self.binaryDirectory = "" self.binaryName = "" self.binaryPath = "" self.version = "" self.installName = "" self.deployedInstallName = "" self.sourceFilePath = "" self.destinationDirectory = "" self.sourceResourcesDirectory = "" self.sourceVersionContentsDirectory = "" self.sourceContentsDirectory = "" self.destinationResourcesDirectory = "" self.destinationVersionContentsDirectory = "" def __eq__(self, other): if self.__class__ == other.__class__: return self.__dict__ == other.__dict__ else: return False def __str__(self): return """ Framework name: {} Framework directory: {} Framework path: {} Binary name: {} Binary directory: {} Binary path: {} Version: {} Install name: {} Deployed install name: {} Source file Path: {} Deployed Directory (relative to bundle): {} """.format(self.frameworkName, - self.frameworkDirectory, - self.frameworkPath, - self.binaryName, - self.binaryDirectory, - self.binaryPath, - self.version, - self.installName, - self.deployedInstallName, - self.sourceFilePath, - self.destinationDirectory) + self.frameworkDirectory, + self.frameworkPath, + self.binaryName, + self.binaryDirectory, + self.binaryPath, + self.version, + self.installName, + self.deployedInstallName, + self.sourceFilePath, + self.destinationDirectory) def isDylib(self): return self.frameworkName.endswith(".dylib") def isQtFramework(self): if self.isDylib(): return self.frameworkName.startswith("libQt") else: return self.frameworkName.startswith("Qt") - reOLine = re.compile(r'^(.+) \(compatibility version [0-9.]+, current version [0-9.]+\)$') + reOLine = re.compile( + r'^(.+) \(compatibility version [0-9.]+, current version [0-9.]+\)$') bundleFrameworkDirectory = "Contents/Frameworks" bundleBinaryDirectory = "Contents/MacOS" @classmethod def fromOtoolLibraryLine(cls, line: str) -> Optional['FrameworkInfo']: # Note: line must be trimmed if line == "": return None - # Don't deploy system libraries (exception for libQtuitools and libQtlucene). - if line.startswith("/System/Library/") or line.startswith("@executable_path") or (line.startswith("/usr/lib/") and "libQt" not in line): + # Don't deploy system libraries (exception for libQtuitools and + # libQtlucene). + if line.startswith("/System/Library/") or line.startswith( + "@executable_path") or (line.startswith("/usr/lib/") and "libQt" not in line): return None m = cls.reOLine.match(line) if m is None: raise RuntimeError("otool line could not be parsed: " + line) path = m.group(1) info = cls() info.sourceFilePath = path info.installName = path if path.endswith(".dylib"): dirname, filename = os.path.split(path) info.frameworkName = filename info.frameworkDirectory = dirname info.frameworkPath = path info.binaryDirectory = dirname info.binaryName = filename info.binaryPath = path info.version = "-" info.installName = path info.deployedInstallName = "@executable_path/../Frameworks/" + info.binaryName info.sourceFilePath = path info.destinationDirectory = cls.bundleFrameworkDirectory else: parts = path.split("/") i = 0 # Search for the .framework directory for part in parts: if part.endswith(".framework"): break i += 1 if i == len(parts): - raise RuntimeError("Could not find .framework or .dylib in otool line: " + line) + raise RuntimeError( + "Could not find .framework or .dylib in otool line: " + line) info.frameworkName = parts[i] info.frameworkDirectory = "/".join(parts[:i]) - info.frameworkPath = os.path.join(info.frameworkDirectory, info.frameworkName) - - info.binaryName = parts[i+3] - info.binaryDirectory = "/".join(parts[i+1:i+3]) - info.binaryPath = os.path.join(info.binaryDirectory, info.binaryName) - info.version = parts[i+2] - - info.deployedInstallName = "@executable_path/../Frameworks/" + os.path.join(info.frameworkName, info.binaryPath) - info.destinationDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, info.binaryDirectory) - - info.sourceResourcesDirectory = os.path.join(info.frameworkPath, "Resources") - info.sourceContentsDirectory = os.path.join(info.frameworkPath, "Contents") - info.sourceVersionContentsDirectory = os.path.join(info.frameworkPath, "Versions", info.version, "Contents") - info.destinationResourcesDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Resources") - info.destinationVersionContentsDirectory = os.path.join(cls.bundleFrameworkDirectory, info.frameworkName, "Versions", info.version, "Contents") + info.frameworkPath = os.path.join( + info.frameworkDirectory, info.frameworkName) + + info.binaryName = parts[i + 3] + info.binaryDirectory = "/".join(parts[i + 1:i + 3]) + info.binaryPath = os.path.join( + info.binaryDirectory, info.binaryName) + info.version = parts[i + 2] + + info.deployedInstallName = "@executable_path/../Frameworks/" + \ + os.path.join(info.frameworkName, info.binaryPath) + info.destinationDirectory = os.path.join( + cls.bundleFrameworkDirectory, info.frameworkName, info.binaryDirectory) + + info.sourceResourcesDirectory = os.path.join( + info.frameworkPath, "Resources") + info.sourceContentsDirectory = os.path.join( + info.frameworkPath, "Contents") + info.sourceVersionContentsDirectory = os.path.join( + info.frameworkPath, "Versions", info.version, "Contents") + info.destinationResourcesDirectory = os.path.join( + cls.bundleFrameworkDirectory, info.frameworkName, "Resources") + info.destinationVersionContentsDirectory = os.path.join( + cls.bundleFrameworkDirectory, + info.frameworkName, + "Versions", + info.version, + "Contents") return info + class ApplicationBundleInfo(object): def __init__(self, path: str): self.path = path appName = "BitcoinABC-Qt" self.binaryPath = os.path.join(path, "Contents", "MacOS", appName) if not os.path.exists(self.binaryPath): raise RuntimeError("Could not find bundle binary for " + path) self.resourcesPath = os.path.join(path, "Contents", "Resources") self.pluginPath = os.path.join(path, "Contents", "PlugIns") + class DeploymentInfo(object): def __init__(self): self.qtPath = None self.pluginPath = None self.deployedFrameworks = [] def detectQtPath(self, frameworkDirectory: str): parentDir = os.path.dirname(frameworkDirectory) if os.path.exists(os.path.join(parentDir, "translations")): # Classic layout, e.g. "/usr/local/Trolltech/Qt-4.x.x" self.qtPath = parentDir else: self.qtPath = os.getenv("QTDIR", None) if self.qtPath is not None: pluginPath = os.path.join(self.qtPath, "plugins") if os.path.exists(pluginPath): self.pluginPath = pluginPath def usesFramework(self, name: str) -> bool: nameDot = "{}.".format(name) libNameDot = "lib{}.".format(name) for framework in self.deployedFrameworks: if framework.endswith(".framework"): if framework.startswith(nameDot): return True elif framework.endswith(".dylib"): if framework.startswith(libNameDot): return True return False + def getFrameworks(binaryPath: str, verbose: int) -> List[FrameworkInfo]: if verbose >= 3: print("Inspecting with otool: " + binaryPath) - otoolbin=os.getenv("OTOOL", "otool") - otool = subprocess.Popen([otoolbin, "-L", binaryPath], stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True) + otoolbin = os.getenv("OTOOL", "otool") + otool = subprocess.Popen([otoolbin, + "-L", + binaryPath], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True) o_stdout, o_stderr = otool.communicate() if otool.returncode != 0: if verbose >= 1: sys.stderr.write(o_stderr) sys.stderr.flush() - raise RuntimeError("otool failed with return code {}".format(otool.returncode)) + raise RuntimeError( + "otool failed with return code {}".format( + otool.returncode)) otoolLines = o_stdout.split("\n") - otoolLines.pop(0) # First line is the inspected binary + otoolLines.pop(0) # First line is the inspected binary if ".framework" in binaryPath or binaryPath.endswith(".dylib"): - otoolLines.pop(0) # Frameworks and dylibs list themselves as a dependency. + # Frameworks and dylibs list themselves as a dependency. + otoolLines.pop(0) libraries = [] for line in otoolLines: line = line.replace("@loader_path", os.path.dirname(binaryPath)) info = FrameworkInfo.fromOtoolLibraryLine(line.strip()) if info is not None: if verbose >= 3: print("Found framework:") print(info) libraries.append(info) return libraries + def runInstallNameTool(action: str, *args): - installnametoolbin=os.getenv("INSTALLNAMETOOL", "install_name_tool") - subprocess.check_call([installnametoolbin, "-"+action] + list(args)) + installnametoolbin = os.getenv("INSTALLNAMETOOL", "install_name_tool") + subprocess.check_call([installnametoolbin, "-" + action] + list(args)) + -def changeInstallName(oldName: str, newName: str, binaryPath: str, verbose: int): +def changeInstallName(oldName: str, newName: str, + binaryPath: str, verbose: int): if verbose >= 3: print("Using install_name_tool:") print(" in", binaryPath) print(" change reference", oldName) print(" to", newName) runInstallNameTool("change", oldName, newName, binaryPath) + def changeIdentification(id: str, binaryPath: str, verbose: int): if verbose >= 3: print("Using install_name_tool:") print(" change identification in", binaryPath) print(" to", id) runInstallNameTool("id", id, binaryPath) + def runStrip(binaryPath: str, verbose: int): - stripbin=os.getenv("STRIP", "strip") + stripbin = os.getenv("STRIP", "strip") if verbose >= 3: print("Using strip:") print(" stripped", binaryPath) subprocess.check_call([stripbin, "-x", binaryPath]) -def copyFramework(framework: FrameworkInfo, path: str, verbose: int) -> Optional[str]: + +def copyFramework(framework: FrameworkInfo, path: str, + verbose: int) -> Optional[str]: if framework.sourceFilePath.startswith("Qt"): - #standard place for Nokia Qt installer's frameworks + # standard place for Nokia Qt installer's frameworks fromPath = "/Library/Frameworks/" + framework.sourceFilePath else: fromPath = framework.sourceFilePath toDir = os.path.join(path, framework.destinationDirectory) toPath = os.path.join(toDir, framework.binaryName) if not os.path.exists(fromPath): raise RuntimeError("No file at " + fromPath) if os.path.exists(toPath): - return None # Already there + return None # Already there if not os.path.exists(toDir): os.makedirs(toDir) shutil.copy2(fromPath, toPath) if verbose >= 3: print("Copied:", fromPath) print(" to:", toPath) permissions = os.stat(toPath) if not permissions.st_mode & stat.S_IWRITE: - os.chmod(toPath, permissions.st_mode | stat.S_IWRITE) + os.chmod(toPath, permissions.st_mode | stat.S_IWRITE) - if not framework.isDylib(): # Copy resources for real frameworks + if not framework.isDylib(): # Copy resources for real frameworks - linkfrom = os.path.join(path, "Contents","Frameworks", framework.frameworkName, "Versions", "Current") + linkfrom = os.path.join( + path, + "Contents", + "Frameworks", + framework.frameworkName, + "Versions", + "Current") linkto = framework.version if not os.path.exists(linkfrom): os.symlink(linkto, linkfrom) if verbose >= 2: print("Linked:", linkfrom, "->", linkto) fromResourcesDir = framework.sourceResourcesDirectory if os.path.exists(fromResourcesDir): - toResourcesDir = os.path.join(path, framework.destinationResourcesDirectory) + toResourcesDir = os.path.join( + path, framework.destinationResourcesDirectory) shutil.copytree(fromResourcesDir, toResourcesDir, symlinks=True) if verbose >= 3: print("Copied resources:", fromResourcesDir) print(" to:", toResourcesDir) fromContentsDir = framework.sourceVersionContentsDirectory if not os.path.exists(fromContentsDir): fromContentsDir = framework.sourceContentsDirectory if os.path.exists(fromContentsDir): - toContentsDir = os.path.join(path, framework.destinationVersionContentsDirectory) + toContentsDir = os.path.join( + path, framework.destinationVersionContentsDirectory) shutil.copytree(fromContentsDir, toContentsDir, symlinks=True) if verbose >= 3: print("Copied Contents:", fromContentsDir) print(" to:", toContentsDir) - elif framework.frameworkName.startswith("libQtGui"): # Copy qt_menu.nib (applies to non-framework layout) - qtMenuNibSourcePath = os.path.join(framework.frameworkDirectory, "Resources", "qt_menu.nib") - qtMenuNibDestinationPath = os.path.join(path, "Contents", "Resources", "qt_menu.nib") - if os.path.exists(qtMenuNibSourcePath) and not os.path.exists(qtMenuNibDestinationPath): - shutil.copytree(qtMenuNibSourcePath, qtMenuNibDestinationPath, symlinks=True) + # Copy qt_menu.nib (applies to non-framework layout) + elif framework.frameworkName.startswith("libQtGui"): + qtMenuNibSourcePath = os.path.join( + framework.frameworkDirectory, "Resources", "qt_menu.nib") + qtMenuNibDestinationPath = os.path.join( + path, "Contents", "Resources", "qt_menu.nib") + if os.path.exists(qtMenuNibSourcePath) and not os.path.exists( + qtMenuNibDestinationPath): + shutil.copytree( + qtMenuNibSourcePath, + qtMenuNibDestinationPath, + symlinks=True) if verbose >= 3: print("Copied for libQtGui:", qtMenuNibSourcePath) print(" to:", qtMenuNibDestinationPath) return toPath -def deployFrameworks(frameworks: List[FrameworkInfo], bundlePath: str, binaryPath: str, strip: bool, verbose: int, deploymentInfo: Optional[DeploymentInfo] = None) -> DeploymentInfo: + +def deployFrameworks(frameworks: List[FrameworkInfo], bundlePath: str, binaryPath: str, strip: bool, + verbose: int, deploymentInfo: Optional[DeploymentInfo] = None) -> DeploymentInfo: if deploymentInfo is None: deploymentInfo = DeploymentInfo() while len(frameworks) > 0: framework = frameworks.pop(0) deploymentInfo.deployedFrameworks.append(framework.frameworkName) if verbose >= 2: print("Processing", framework.frameworkName, "...") # Get the Qt path from one of the Qt frameworks if deploymentInfo.qtPath is None and framework.isQtFramework(): deploymentInfo.detectQtPath(framework.frameworkDirectory) - if framework.installName.startswith("@executable_path") or framework.installName.startswith(bundlePath): + if framework.installName.startswith( + "@executable_path") or framework.installName.startswith(bundlePath): if verbose >= 2: print(framework.frameworkName, "already deployed, skipping.") continue # install_name_tool the new id into the binary - changeInstallName(framework.installName, framework.deployedInstallName, binaryPath, verbose) + changeInstallName( + framework.installName, + framework.deployedInstallName, + binaryPath, + verbose) # Copy framework to app bundle. deployedBinaryPath = copyFramework(framework, bundlePath, verbose) # Skip the rest if already was deployed. if deployedBinaryPath is None: continue if strip: runStrip(deployedBinaryPath, verbose) # install_name_tool it a new id. - changeIdentification(framework.deployedInstallName, deployedBinaryPath, verbose) + changeIdentification( + framework.deployedInstallName, + deployedBinaryPath, + verbose) # Check for framework dependencies dependencies = getFrameworks(deployedBinaryPath, verbose) for dependency in dependencies: - changeInstallName(dependency.installName, dependency.deployedInstallName, deployedBinaryPath, verbose) + changeInstallName( + dependency.installName, + dependency.deployedInstallName, + deployedBinaryPath, + verbose) # Deploy framework if necessary. if dependency.frameworkName not in deploymentInfo.deployedFrameworks and dependency not in frameworks: frameworks.append(dependency) return deploymentInfo -def deployFrameworksForAppBundle(applicationBundle: ApplicationBundleInfo, strip: bool, verbose: int) -> DeploymentInfo: + +def deployFrameworksForAppBundle( + applicationBundle: ApplicationBundleInfo, strip: bool, verbose: int) -> DeploymentInfo: frameworks = getFrameworks(applicationBundle.binaryPath, verbose) if len(frameworks) == 0 and verbose >= 1: - print("Warning: Could not find any external frameworks to deploy in {}.".format(applicationBundle.path)) + print( + "Warning: Could not find any external frameworks to deploy in {}.".format( + applicationBundle.path)) return DeploymentInfo() else: - return deployFrameworks(frameworks, applicationBundle.path, applicationBundle.binaryPath, strip, verbose) + return deployFrameworks( + frameworks, applicationBundle.path, applicationBundle.binaryPath, strip, verbose) + -def deployPlugins(appBundleInfo: ApplicationBundleInfo, deploymentInfo: DeploymentInfo, strip: bool, verbose: int): +def deployPlugins(appBundleInfo: ApplicationBundleInfo, + deploymentInfo: DeploymentInfo, strip: bool, verbose: int): # Lookup available plugins, exclude unneeded plugins = [] if deploymentInfo.pluginPath is None: return for dirpath, dirnames, filenames in os.walk(deploymentInfo.pluginPath): pluginDirectory = os.path.relpath(dirpath, deploymentInfo.pluginPath) if pluginDirectory == "designer": # Skip designer plugins continue elif pluginDirectory == "printsupport": # Skip printsupport plugins continue elif pluginDirectory == "imageformats": # Skip imageformats plugins continue elif pluginDirectory == "sqldrivers": # Deploy the sql plugins only if QtSql is in use if not deploymentInfo.usesFramework("QtSql"): continue elif pluginDirectory == "script": # Deploy the script plugins only if QtScript is in use if not deploymentInfo.usesFramework("QtScript"): continue elif pluginDirectory == "qmltooling" or pluginDirectory == "qml1tooling": # Deploy the qml plugins only if QtDeclarative is in use if not deploymentInfo.usesFramework("QtDeclarative"): continue elif pluginDirectory == "bearer": # Deploy the bearer plugins only if QtNetwork is in use if not deploymentInfo.usesFramework("QtNetwork"): continue elif pluginDirectory == "position": # Deploy the position plugins only if QtPositioning is in use if not deploymentInfo.usesFramework("QtPositioning"): continue elif pluginDirectory == "sensors" or pluginDirectory == "sensorgestures": # Deploy the sensor plugins only if QtSensors is in use if not deploymentInfo.usesFramework("QtSensors"): continue elif pluginDirectory == "audio" or pluginDirectory == "playlistformats": # Deploy the audio plugins only if QtMultimedia is in use if not deploymentInfo.usesFramework("QtMultimedia"): continue elif pluginDirectory == "mediaservice": - # Deploy the mediaservice plugins only if QtMultimediaWidgets is in use + # Deploy the mediaservice plugins only if QtMultimediaWidgets is in + # use if not deploymentInfo.usesFramework("QtMultimediaWidgets"): continue elif pluginDirectory == "canbus": # Deploy the canbus plugins only if QtSerialBus is in use if not deploymentInfo.usesFramework("QtSerialBus"): continue elif pluginDirectory == "webview": # Deploy the webview plugins only if QtWebView is in use if not deploymentInfo.usesFramework("QtWebView"): continue elif pluginDirectory == "gamepads": # Deploy the webview plugins only if QtGamepad is in use if not deploymentInfo.usesFramework("QtGamepad"): continue elif pluginDirectory == "geoservices": # Deploy the webview plugins only if QtLocation is in use if not deploymentInfo.usesFramework("QtLocation"): continue elif pluginDirectory == "texttospeech": # Deploy the texttospeech plugins only if QtTextToSpeech is in use if not deploymentInfo.usesFramework("QtTextToSpeech"): continue elif pluginDirectory == "virtualkeyboard": - # Deploy the virtualkeyboard plugins only if QtVirtualKeyboard is in use + # Deploy the virtualkeyboard plugins only if QtVirtualKeyboard is + # in use if not deploymentInfo.usesFramework("QtVirtualKeyboard"): continue elif pluginDirectory == "sceneparsers": # Deploy the virtualkeyboard plugins only if Qt3DCore is in use if not deploymentInfo.usesFramework("Qt3DCore"): continue elif pluginDirectory == "renderplugins": # Deploy the renderplugins plugins only if Qt3DCore is in use if not deploymentInfo.usesFramework("Qt3DCore"): continue elif pluginDirectory == "geometryloaders": # Deploy the geometryloaders plugins only if Qt3DCore is in use if not deploymentInfo.usesFramework("Qt3DCore"): continue for pluginName in filenames: pluginPath = os.path.join(pluginDirectory, pluginName) if pluginName.endswith("_debug.dylib"): # Skip debug plugins continue elif pluginPath == "imageformats/libqsvg.dylib" or pluginPath == "iconengines/libqsvgicon.dylib": # Deploy the svg plugins only if QtSvg is in use if not deploymentInfo.usesFramework("QtSvg"): continue elif pluginPath == "accessible/libqtaccessiblecompatwidgets.dylib": - # Deploy accessibility for Qt3Support only if the Qt3Support is in use + # Deploy accessibility for Qt3Support only if the Qt3Support is + # in use if not deploymentInfo.usesFramework("Qt3Support"): continue elif pluginPath == "graphicssystems/libqglgraphicssystem.dylib": - # Deploy the opengl graphicssystem plugin only if QtOpenGL is in use + # Deploy the opengl graphicssystem plugin only if QtOpenGL is + # in use if not deploymentInfo.usesFramework("QtOpenGL"): continue elif pluginPath == "accessible/libqtaccessiblequick.dylib": - # Deploy the accessible qtquick plugin only if QtQuick is in use + # Deploy the accessible qtquick plugin only if QtQuick is in + # use if not deploymentInfo.usesFramework("QtQuick"): continue elif pluginPath == "platforminputcontexts/libqtvirtualkeyboardplugin.dylib": - # Deploy the virtualkeyboardplugin plugin only if QtVirtualKeyboard is in use + # Deploy the virtualkeyboardplugin plugin only if + # QtVirtualKeyboard is in use if not deploymentInfo.usesFramework("QtVirtualKeyboard"): continue plugins.append((pluginDirectory, pluginName)) for pluginDirectory, pluginName in plugins: if verbose >= 2: - print("Processing plugin", os.path.join(pluginDirectory, pluginName), "...") - - sourcePath = os.path.join(deploymentInfo.pluginPath, pluginDirectory, pluginName) - destinationDirectory = os.path.join(appBundleInfo.pluginPath, pluginDirectory) + print( + "Processing plugin", + os.path.join( + pluginDirectory, + pluginName), + "...") + + sourcePath = os.path.join( + deploymentInfo.pluginPath, + pluginDirectory, + pluginName) + destinationDirectory = os.path.join( + appBundleInfo.pluginPath, pluginDirectory) if not os.path.exists(destinationDirectory): os.makedirs(destinationDirectory) destinationPath = os.path.join(destinationDirectory, pluginName) shutil.copy2(sourcePath, destinationPath) if verbose >= 3: print("Copied:", sourcePath) print(" to:", destinationPath) if strip: runStrip(destinationPath, verbose) dependencies = getFrameworks(destinationPath, verbose) for dependency in dependencies: - changeInstallName(dependency.installName, dependency.deployedInstallName, destinationPath, verbose) + changeInstallName( + dependency.installName, + dependency.deployedInstallName, + destinationPath, + verbose) # Deploy framework if necessary. if dependency.frameworkName not in deploymentInfo.deployedFrameworks: - deployFrameworks([dependency], appBundleInfo.path, destinationPath, strip, verbose, deploymentInfo) + deployFrameworks( + [dependency], + appBundleInfo.path, + destinationPath, + strip, + verbose, + deploymentInfo) + -qt_conf="""[Paths] +qt_conf = """[Paths] Translations=Resources Plugins=PlugIns """ ap = ArgumentParser(description="""Improved version of macdeployqt. Outputs a ready-to-deploy app in a folder "dist" and optionally wraps it in a .dmg file. Note, that the "dist" folder will be deleted before deploying on each run. Optionally, Qt translation files (.qm) and additional resources can be added to the bundle. Also optionally signs the .app bundle; set the CODESIGNARGS environment variable to pass arguments to the codesign tool. E.g. CODESIGNARGS='--sign "Developer ID Application: ..." --keychain /encrypted/foo.keychain'""") -ap.add_argument("app_bundle", nargs=1, metavar="app-bundle", help="application bundle to be deployed") -ap.add_argument("-verbose", type=int, nargs=1, default=[1], metavar="<0-3>", help="0 = no output, 1 = error/warning (default), 2 = normal, 3 = debug") -ap.add_argument("-no-plugins", dest="plugins", action="store_false", default=True, help="skip plugin deployment") -ap.add_argument("-no-strip", dest="strip", action="store_false", default=True, help="don't run 'strip' on the binaries") -ap.add_argument("-sign", dest="sign", action="store_true", default=False, help="sign .app bundle with codesign tool") -ap.add_argument("-dmg", nargs="?", const="", metavar="basename", help="create a .dmg disk image; if basename is not specified, a camel-cased version of the app name is used") -ap.add_argument("-fancy", nargs=1, metavar="plist", default=[], help="make a fancy looking disk image using the given plist file with instructions; requires -dmg to work") -ap.add_argument("-add-qt-tr", nargs=1, metavar="languages", default=[], help="add Qt translation files to the bundle's resources; the language list must be separated with commas, not with whitespace") -ap.add_argument("-translations-dir", nargs=1, metavar="path", default=None, help="Path to Qt's translation files") -ap.add_argument("-add-resources", nargs="+", metavar="path", default=[], help="list of additional files or folders to be copied into the bundle's resources; must be the last argument") -ap.add_argument("-volname", nargs=1, metavar="volname", default=[], help="custom volume name for dmg") +ap.add_argument("app_bundle", nargs=1, metavar="app-bundle", + help="application bundle to be deployed") +ap.add_argument( + "-verbose", + type=int, + nargs=1, + default=[1], + metavar="<0-3>", + help="0 = no output, 1 = error/warning (default), 2 = normal, 3 = debug") +ap.add_argument( + "-no-plugins", + dest="plugins", + action="store_false", + default=True, + help="skip plugin deployment") +ap.add_argument( + "-no-strip", + dest="strip", + action="store_false", + default=True, + help="don't run 'strip' on the binaries") +ap.add_argument( + "-sign", + dest="sign", + action="store_true", + default=False, + help="sign .app bundle with codesign tool") +ap.add_argument( + "-dmg", + nargs="?", + const="", + metavar="basename", + help="create a .dmg disk image; if basename is not specified, a camel-cased version of the app name is used") +ap.add_argument( + "-fancy", + nargs=1, + metavar="plist", + default=[], + help="make a fancy looking disk image using the given plist file with instructions; requires -dmg to work") +ap.add_argument( + "-add-qt-tr", + nargs=1, + metavar="languages", + default=[], + help="add Qt translation files to the bundle's resources; the language list must be separated with commas, not with whitespace") +ap.add_argument( + "-translations-dir", + nargs=1, + metavar="path", + default=None, + help="Path to Qt's translation files") +ap.add_argument( + "-add-resources", + nargs="+", + metavar="path", + default=[], + help="list of additional files or folders to be copied into the bundle's resources; must be the last argument") +ap.add_argument( + "-volname", + nargs=1, + metavar="volname", + default=[], + help="custom volume name for dmg") config = ap.parse_args() verbose = config.verbose[0] # ------------------------------------------------ app_bundle = config.app_bundle[0] if not os.path.exists(app_bundle): if verbose >= 1: - sys.stderr.write("Error: Could not find app bundle \"{}\"\n".format(app_bundle)) + sys.stderr.write( + "Error: Could not find app bundle \"{}\"\n".format(app_bundle)) sys.exit(1) app_bundle_name = os.path.splitext(os.path.basename(app_bundle))[0] # ------------------------------------------------ translations_dir = None if config.translations_dir and config.translations_dir[0]: if os.path.exists(config.translations_dir[0]): translations_dir = config.translations_dir[0] else: if verbose >= 1: - sys.stderr.write("Error: Could not find translation dir \"{}\"\n".format(translations_dir)) + sys.stderr.write( + "Error: Could not find translation dir \"{}\"\n".format(translations_dir)) sys.exit(1) # ------------------------------------------------ for p in config.add_resources: if verbose >= 3: - print("Checking for \"%s\"..." % p) + print("Checking for \"{}\"...".format(p)) if not os.path.exists(p): if verbose >= 1: - sys.stderr.write("Error: Could not find additional resource file \"{}\"\n".format(p)) + sys.stderr.write( + "Error: Could not find additional resource file \"{}\"\n".format(p)) sys.exit(1) # ------------------------------------------------ if len(config.fancy) == 1: if verbose >= 3: print("Fancy: Importing plistlib...") try: import plistlib except ImportError: if verbose >= 1: - sys.stderr.write("Error: Could not import plistlib which is required for fancy disk images.\n") + sys.stderr.write( + "Error: Could not import plistlib which is required for fancy disk images.\n") sys.exit(1) p = config.fancy[0] if verbose >= 3: print("Fancy: Loading \"{}\"...".format(p)) if not os.path.exists(p): if verbose >= 1: - sys.stderr.write("Error: Could not find fancy disk image plist at \"{}\"\n".format(p)) + sys.stderr.write( + "Error: Could not find fancy disk image plist at \"{}\"\n".format(p)) sys.exit(1) try: fancy = plistlib.readPlist(p) - except: + except BaseException: if verbose >= 1: - sys.stderr.write("Error: Could not parse fancy disk image plist at \"{}\"\n".format(p)) + sys.stderr.write( + "Error: Could not parse fancy disk image plist at \"{}\"\n".format(p)) sys.exit(1) try: - assert "window_bounds" not in fancy or (isinstance(fancy["window_bounds"], list) and len(fancy["window_bounds"]) == 4) - assert "background_picture" not in fancy or isinstance(fancy["background_picture"], str) + assert "window_bounds" not in fancy or ( + isinstance( + fancy["window_bounds"], + list) and len( + fancy["window_bounds"]) == 4) + assert "background_picture" not in fancy or isinstance( + fancy["background_picture"], str) assert "icon_size" not in fancy or isinstance(fancy["icon_size"], int) - assert "applications_symlink" not in fancy or isinstance(fancy["applications_symlink"], bool) + assert "applications_symlink" not in fancy or isinstance( + fancy["applications_symlink"], bool) if "items_position" in fancy: assert isinstance(fancy["items_position"], dict) for key, value in fancy["items_position"].items(): - assert isinstance(value, list) and len(value) == 2 and isinstance(value[0], int) and isinstance(value[1], int) - except: + assert isinstance( + value, + list) and len(value) == 2 and isinstance( + value[0], + int) and isinstance( + value[1], + int) + except BaseException: if verbose >= 1: - sys.stderr.write("Error: Bad format of fancy disk image plist at \"{}\"\n".format(p)) + sys.stderr.write( + "Error: Bad format of fancy disk image plist at \"{}\"\n".format(p)) sys.exit(1) if "background_picture" in fancy: bp = fancy["background_picture"] if verbose >= 3: print("Fancy: Resolving background picture \"{}\"...".format(bp)) if not os.path.exists(bp): bp = os.path.join(os.path.dirname(p), bp) if not os.path.exists(bp): if verbose >= 1: - sys.stderr.write("Error: Could not find background picture at \"{}\" or \"{}\"\n".format(fancy["background_picture"], bp)) + sys.stderr.write( + "Error: Could not find background picture at \"{}\" or \"{}\"\n".format( + fancy["background_picture"], bp)) sys.exit(1) else: fancy["background_picture"] = bp else: fancy = None # ------------------------------------------------ if os.path.exists("dist"): if verbose >= 2: print("+ Removing old dist folder +") shutil.rmtree("dist") # ------------------------------------------------ if len(config.volname) == 1: volname = config.volname[0] else: volname = app_bundle_name # ------------------------------------------------ target = os.path.join("dist", "BitcoinABC-Qt.app") if verbose >= 2: print("+ Copying source bundle +") if verbose >= 3: print(app_bundle, "->", target) os.mkdir("dist") shutil.copytree(app_bundle, target, symlinks=True) applicationBundle = ApplicationBundleInfo(target) # ------------------------------------------------ if verbose >= 2: print("+ Deploying frameworks +") try: - deploymentInfo = deployFrameworksForAppBundle(applicationBundle, config.strip, verbose) + deploymentInfo = deployFrameworksForAppBundle( + applicationBundle, config.strip, verbose) if deploymentInfo.qtPath is None: deploymentInfo.qtPath = os.getenv("QTDIR", None) if deploymentInfo.qtPath is None: if verbose >= 1: - sys.stderr.write("Warning: Could not detect Qt's path, skipping plugin deployment!\n") + sys.stderr.write( + "Warning: Could not detect Qt's path, skipping plugin deployment!\n") config.plugins = False except RuntimeError as e: if verbose >= 1: sys.stderr.write("Error: {}\n".format(str(e))) sys.exit(1) # ------------------------------------------------ if config.plugins: if verbose >= 2: print("+ Deploying plugins +") try: deployPlugins(applicationBundle, deploymentInfo, config.strip, verbose) except RuntimeError as e: if verbose >= 1: sys.stderr.write("Error: {}\n".format(str(e))) sys.exit(1) # ------------------------------------------------ if len(config.add_qt_tr) == 0: add_qt_tr = [] else: if translations_dir is not None: qt_tr_dir = translations_dir else: if deploymentInfo.qtPath is not None: qt_tr_dir = os.path.join(deploymentInfo.qtPath, "translations") else: sys.stderr.write("Error: Could not find Qt translation path\n") sys.exit(1) - add_qt_tr = ["qt_{}.qm".format(lng) for lng in config.add_qt_tr[0].split(",")] + add_qt_tr = ["qt_{}.qm".format(lng) + for lng in config.add_qt_tr[0].split(",")] for lng_file in add_qt_tr: p = os.path.join(qt_tr_dir, lng_file) if verbose >= 3: print("Checking for \"{}\"...".format(p)) if not os.path.exists(p): if verbose >= 1: - sys.stderr.write("Error: Could not find Qt translation file \"{}\"\n".format(lng_file)) + sys.stderr.write( + "Error: Could not find Qt translation file \"{}\"\n".format(lng_file)) sys.exit(1) # ------------------------------------------------ if verbose >= 2: print("+ Installing qt.conf +") with open(os.path.join(applicationBundle.resourcesPath, "qt.conf"), "wb") as f: f.write(qt_conf.encode()) # ------------------------------------------------ if len(add_qt_tr) > 0 and verbose >= 2: print("+ Adding Qt translations +") for lng_file in add_qt_tr: if verbose >= 3: - print(os.path.join(qt_tr_dir, lng_file), "->", os.path.join(applicationBundle.resourcesPath, lng_file)) - shutil.copy2(os.path.join(qt_tr_dir, lng_file), os.path.join(applicationBundle.resourcesPath, lng_file)) + print( + os.path.join( + qt_tr_dir, + lng_file), + "->", + os.path.join( + applicationBundle.resourcesPath, + lng_file)) + shutil.copy2( + os.path.join( + qt_tr_dir, lng_file), os.path.join( + applicationBundle.resourcesPath, lng_file)) # ------------------------------------------------ if len(config.add_resources) > 0 and verbose >= 2: print("+ Adding additional resources +") for p in config.add_resources: t = os.path.join(applicationBundle.resourcesPath, os.path.basename(p)) if verbose >= 3: print(p, "->", t) if os.path.isdir(p): shutil.copytree(p, t, symlinks=True) else: shutil.copy2(p, t) # ------------------------------------------------ if config.sign and 'CODESIGNARGS' not in os.environ: print("You must set the CODESIGNARGS environment variable. Skipping signing.") elif config.sign: if verbose >= 1: print("Code-signing app bundle {}".format(target)) - subprocess.check_call("codesign --force {} {}".format(os.environ['CODESIGNARGS'], target), shell=True) + subprocess.check_call( + "codesign --force {} {}".format(os.environ['CODESIGNARGS'], target), shell=True) # ------------------------------------------------ if config.dmg is not None: def runHDIUtil(verb: str, image_basename: str, **kwargs) -> int: hdiutil_args = ["hdiutil", verb, image_basename + ".dmg"] if "capture_stdout" in kwargs: del kwargs["capture_stdout"] run = subprocess.check_output else: if verbose < 2: hdiutil_args.append("-quiet") elif verbose >= 3: hdiutil_args.append("-verbose") run = subprocess.check_call for key, value in kwargs.items(): hdiutil_args.append("-" + key) if value is not True: hdiutil_args.append(str(value)) return run(hdiutil_args, universal_newlines=True) if verbose >= 2: if fancy is None: print("+ Creating .dmg disk image +") else: print("+ Preparing .dmg disk image +") if config.dmg != "": dmg_name = config.dmg else: spl = app_bundle_name.split(" ") dmg_name = spl[0] + "".join(p.capitalize() for p in spl[1:]) if fancy is None: try: - runHDIUtil("create", dmg_name, srcfolder="dist", format="UDBZ", volname=volname, ov=True) + runHDIUtil( + "create", + dmg_name, + srcfolder="dist", + format="UDBZ", + volname=volname, + ov=True) except subprocess.CalledProcessError as e: sys.exit(e.returncode) else: if verbose >= 3: print("Determining size of \"dist\"...") size = 0 for path, dirs, files in os.walk("dist"): for file in files: size += os.path.getsize(os.path.join(path, file)) size += int(size * 0.15) if verbose >= 3: print("Creating temp image for modification...") try: - runHDIUtil("create", dmg_name + ".temp", srcfolder="dist", format="UDRW", size=size, volname=volname, ov=True) + runHDIUtil( + "create", + dmg_name + ".temp", + srcfolder="dist", + format="UDRW", + size=size, + volname=volname, + ov=True) except subprocess.CalledProcessError as e: sys.exit(e.returncode) if verbose >= 3: print("Attaching temp image...") try: - output = runHDIUtil("attach", dmg_name + ".temp", readwrite=True, noverify=True, noautoopen=True, capture_stdout=True) + output = runHDIUtil( + "attach", + dmg_name + ".temp", + readwrite=True, + noverify=True, + noautoopen=True, + capture_stdout=True) except subprocess.CalledProcessError as e: sys.exit(e.returncode) m = re.search("/Volumes/(.+$)", output) disk_root = m.group(0) disk_name = m.group(1) if verbose >= 2: print("+ Applying fancy settings +") if "background_picture" in fancy: - bg_path = os.path.join(disk_root, ".background", os.path.basename(fancy["background_picture"])) + bg_path = os.path.join( + disk_root, ".background", os.path.basename( + fancy["background_picture"])) os.mkdir(os.path.dirname(bg_path)) if verbose >= 3: print(fancy["background_picture"], "->", bg_path) shutil.copy2(fancy["background_picture"], bg_path) else: bg_path = None if fancy.get("applications_symlink", False): - os.symlink("/Applications", os.path.join(disk_root, "Applications")) + os.symlink( + "/Applications", + os.path.join( + disk_root, + "Applications")) # The Python appscript package broke with OSX 10.8 and isn't being fixed. # So we now build up an AppleScript string and use the osascript command # to make the .dmg file pretty: - appscript = Template( """ + appscript = Template(""" on run argv tell application "Finder" tell disk "$disk" open set current view of container window to icon view set toolbar visible of container window to false set statusbar visible of container window to false set the bounds of container window to {$window_bounds} set theViewOptions to the icon view options of container window set arrangement of theViewOptions to not arranged set icon size of theViewOptions to $icon_size $background_commands $items_positions close -- close/reopen works around a bug... open update without registering applications delay 5 eject end tell end tell end run """) - itemscript = Template('set position of item "${item}" of container window to {${position}}') + itemscript = Template( + 'set position of item "${item}" of container window to {${position}}') items_positions = [] if "items_position" in fancy: for name, position in fancy["items_position"].items(): - params = { "item" : name, "position" : ",".join([str(p) for p in position]) } + params = {"item": name, "position": ",".join( + [str(p) for p in position])} items_positions.append(itemscript.substitute(params)) params = { - "disk" : volname, - "window_bounds" : "300,300,800,620", - "icon_size" : "96", - "background_commands" : "", - "items_positions" : "\n ".join(items_positions) - } + "disk": volname, + "window_bounds": "300,300,800,620", + "icon_size": "96", + "background_commands": "", + "items_positions": "\n ".join(items_positions) + } if "window_bounds" in fancy: - params["window_bounds"] = ",".join([str(p) for p in fancy["window_bounds"]]) + params["window_bounds"] = ",".join( + [str(p) for p in fancy["window_bounds"]]) if "icon_size" in fancy: params["icon_size"] = str(fancy["icon_size"]) if bg_path is not None: # Set background file, then call SetFile to make it invisible. # (note: making it invisible first makes set background picture fail) bgscript = Template("""set background picture of theViewOptions to file ".background:$bgpic" do shell script "SetFile -a V /Volumes/$disk/.background/$bgpic" """) - params["background_commands"] = bgscript.substitute({"bgpic" : os.path.basename(bg_path), "disk" : params["disk"]}) + params["background_commands"] = bgscript.substitute( + {"bgpic": os.path.basename(bg_path), "disk": params["disk"]}) s = appscript.substitute(params) if verbose >= 2: print("Running AppleScript:") print(s) p = subprocess.Popen(['osascript', '-'], stdin=subprocess.PIPE) p.communicate(input=s.encode('utf-8')) if p.returncode: print("Error running osascript.") if verbose >= 2: print("+ Finalizing .dmg disk image +") time.sleep(5) try: - runHDIUtil("convert", dmg_name + ".temp", format="UDBZ", o=dmg_name + ".dmg", ov=True) + runHDIUtil( + "convert", + dmg_name + ".temp", + format="UDBZ", + o=dmg_name + ".dmg", + ov=True) except subprocess.CalledProcessError as e: sys.exit(e.returncode) os.unlink(dmg_name + ".temp.dmg") # ------------------------------------------------ if verbose >= 2: print("+ Done +") sys.exit(0) diff --git a/src/qt/CMakeLists.txt b/src/qt/CMakeLists.txt index fde241089..baa1c5b46 100644 --- a/src/qt/CMakeLists.txt +++ b/src/qt/CMakeLists.txt @@ -1,626 +1,626 @@ # Copyright (c) 2018 The Bitcoin developers project(bitcoin-qt) include(BrewHelper) find_brew_prefix(QT5_PREFIX qt5) set(QT_REQUIRED_COMPONENTS Core Widgets Network Test) if(ENABLE_DBUS_NOTIFICATIONS) list(APPEND QT_REQUIRED_COMPONENTS DBus) endif() find_package(Qt5 5.9.5 COMPONENTS ${QT_REQUIRED_COMPONENTS} REQUIRED HINTS "${QT5_PREFIX}") # Localisation add_subdirectory(locale) add_custom_command(OUTPUT temp_bitcoin_locale.qrc COMMAND cmake ARGS -E copy "${CMAKE_CURRENT_SOURCE_DIR}/bitcoin_locale.qrc" temp_bitcoin_locale.qrc MAIN_DEPENDENCY bitcoin_locale.qrc VERBATIM ) add_custom_command(OUTPUT qrc_bitcoin_locale.cpp COMMAND Qt5::rcc ARGS temp_bitcoin_locale.qrc -name bitcoin_locale -o qrc_bitcoin_locale.cpp MAIN_DEPENDENCY temp_bitcoin_locale.qrc DEPENDS locales VERBATIM ) # UI elements # qt5_wrap_ui() generates the files in the CMAKE_CURRENT_BINARY_DIR. As there # is no option to change the output directory, moving the files to the forms # subdirectory requires to override the variable. It is reset to its actual # value after the call so it does not impact the other sections of this # CMakeLists.txt file. set(SAVE_CMAKE_CURRENT_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}) set(CMAKE_CURRENT_BINARY_DIR "${CMAKE_CURRENT_BINARY_DIR}/forms") # It seems that some generators (at least the Unix Makefiles one) doesn't create # the build directory required by a custom command, so do it manually. file(MAKE_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}) qt5_wrap_ui(UI_GENERATED_HEADERS forms/addressbookpage.ui forms/askpassphrasedialog.ui forms/coincontroldialog.ui forms/createwalletdialog.ui forms/editaddressdialog.ui forms/helpmessagedialog.ui forms/intro.ui forms/modaloverlay.ui forms/openuridialog.ui forms/optionsdialog.ui forms/overviewpage.ui forms/receivecoinsdialog.ui forms/receiverequestdialog.ui forms/debugwindow.ui forms/sendcoinsdialog.ui forms/sendcoinsentry.ui forms/signverifymessagedialog.ui forms/transactiondescdialog.ui ) set(CMAKE_CURRENT_BINARY_DIR ${SAVE_CMAKE_CURRENT_BINARY_DIR}) # Qt MOC set(CMAKE_AUTOMOC ON) # Handle qrc resources qt5_add_resources(QRC_BITCOIN_CPP bitcoin.qrc) add_library(bitcoin-qt-base bantablemodel.cpp bitcoin.cpp bitcoinaddressvalidator.cpp bitcoinamountfield.cpp bitcoingui.cpp bitcoinunits.cpp clientmodel.cpp csvmodelwriter.cpp guiutil.cpp intro.cpp modaloverlay.cpp networkstyle.cpp notificator.cpp optionsdialog.cpp optionsmodel.cpp peertablemodel.cpp platformstyle.cpp qvalidatedlineedit.cpp qvaluecombobox.cpp rpcconsole.cpp splashscreen.cpp trafficgraphwidget.cpp utilitydialog.cpp # Handle ui files ${UI_GENERATED_HEADERS} # Translations ${BITCOIN_QM_FILES} # Handle qrc files ${QRC_BITCOIN_CPP} qrc_bitcoin_locale.cpp ) # Add the minimal integration plugin, and other plugins according to the target # platform. set(QT_PLUGIN_COMPONENTS QMinimalIntegrationPlugin) set(QT_PLUGIN_PLATFORM_DEFINITIONS -DQT_QPA_PLATFORM_MINIMAL=1) # Linux support if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") list(APPEND QT_PLUGIN_COMPONENTS QXcbIntegrationPlugin) list(APPEND QT_PLUGIN_PLATFORM_DEFINITIONS -DQT_QPA_PLATFORM_XCB=1) endif() # Windows support if(${CMAKE_SYSTEM_NAME} MATCHES "Windows") list(APPEND QT_PLUGIN_COMPONENTS QWindowsIntegrationPlugin) list(APPEND QT_PLUGIN_PLATFORM_DEFINITIONS -DQT_QPA_PLATFORM_WINDOWS=1) target_sources(bitcoin-qt-base PRIVATE winshutdownmonitor.cpp) endif() # OSX support if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") list(APPEND QT_PLUGIN_COMPONENTS QCocoaIntegrationPlugin) list(APPEND QT_PLUGIN_PLATFORM_DEFINITIONS -DQT_QPA_PLATFORM_COCOA=1) target_sources(bitcoin-qt-base PRIVATE macdockiconhandler.mm macnotificationhandler.mm ) set_property(TARGET bitcoin-qt-base PROPERTY AUTOMOC_MOC_OPTIONS "-DQ_OS_MAC") target_link_libraries(bitcoin-qt-base "-framework Foundation" "-framework ApplicationServices" "-framework AppKit" ) endif() # Find out more about Qt. This is similar to # http://code.qt.io/cgit/qt/qtwebkit.git/tree/Source/cmake/OptionsQt.cmake get_target_property(QT_CORE_TYPE Qt5::Core TYPE) if(QT_CORE_TYPE MATCHES STATIC) set(QT_STATIC_BUILD ON) endif() # Determine the Qt libraries directory from the QT5::Core library location get_target_property(QT_CORE_LIB_LOCATION Qt5::Core LOCATION) get_filename_component(QT5_LIB_DIR "${QT_CORE_LIB_LOCATION}" DIRECTORY) set(STATIC_DEPENDENCIES_CMAKE_FILE "${CMAKE_BINARY_DIR}/QtStaticDependencies.cmake") if(EXISTS ${STATIC_DEPENDENCIES_CMAKE_FILE}) file(REMOVE ${STATIC_DEPENDENCIES_CMAKE_FILE}) endif() set(CONVERT_PRL_PATH "${CONTRIB_PATH}/qt/convert-prl-libs-to-cmake.pl") macro(CONVERT_PRL_LIBS_TO_CMAKE _qt_component) if(TARGET Qt5::${_qt_component}) get_target_property(_lib_location Qt5::${_qt_component} LOCATION) execute_process(COMMAND ${PERL_EXECUTABLE} "${CONVERT_PRL_PATH}" --lib "${_lib_location}" --qt_lib_install_dir "${QT5_LIB_DIR}" --out "${STATIC_DEPENDENCIES_CMAKE_FILE}" --component "${_qt_component}" --compiler "${CMAKE_CXX_COMPILER_ID}" ) endif() endmacro() if(QT_STATIC_BUILD) list(APPEND QT_REQUIRED_COMPONENTS ${QT_PLUGIN_COMPONENTS}) foreach(qt_module ${QT_REQUIRED_COMPONENTS}) CONVERT_PRL_LIBS_TO_CMAKE(${qt_module}) endforeach() # HACK: We must explicitly add LIB path of the Qt installation # to correctly find qtpcre link_directories("${QT5_LIB_DIR}") # Now that we generated the dependencies, import them. set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CONVERT_PRL_PATH}") if(NOT EXISTS ${STATIC_DEPENDENCIES_CMAKE_FILE}) message(FATAL_ERROR "Unable to find ${STATIC_DEPENDENCIES_CMAKE_FILE}") endif() include(${STATIC_DEPENDENCIES_CMAKE_FILE}) list(REMOVE_DUPLICATES STATIC_LIB_DEPENDENCIES) # According to Qt documentation (https://doc.qt.io/qt-5/plugins-howto.html): # "Plugins can be linked statically into your application. # If you build the static version of Qt, this is the only option for # including Qt's predefined plugins." # So if the Qt build is static, the plugins should also be static and we # need to define QT_STATICPLUGIN to tell the code to import . target_compile_definitions(bitcoin-qt-base PUBLIC -DQT_STATICPLUGIN=1) # Add the platform plugin definition if required # Setting this definition tells the code what is the target for Q_IMPORT_PLUGIN(). foreach(qt_platform_definition ${QT_PLUGIN_PLATFORM_DEFINITIONS}) target_compile_definitions(bitcoin-qt-base PUBLIC "${qt_platform_definition}") endforeach() # Link the required plugins foreach(qt_plugin ${QT_PLUGIN_COMPONENTS}) target_link_libraries(bitcoin-qt-base Qt5::${qt_plugin}) endforeach() endif() target_link_libraries(bitcoin-qt-base server rpcclient Qt5::Widgets Qt5::Network ) if(ENABLE_DBUS_NOTIFICATIONS) target_link_libraries(bitcoin-qt-base Qt5::DBus) endif() if(ENABLE_BIP70) # Do protobuf codegen find_package(Protobuf REQUIRED) protobuf_generate_cpp(PROTOBUF_SOURCES PROTOBUF_HEADERS paymentrequest.proto) add_library(bitcoin-qt-protobuf OBJECT # Protobuf codegen ${PROTOBUF_HEADERS} ${PROTOBUF_SOURCES} ) target_include_directories(bitcoin-qt-protobuf PUBLIC ${Protobuf_INCLUDE_DIRS}) target_link_libraries(bitcoin-qt-protobuf ${Protobuf_LIBRARIES}) # Don't run clang-tidy on generated files if(ENABLE_CLANG_TIDY) include(ClangTidy) target_disable_clang_tidy(bitcoin-qt-protobuf) endif() # Message::ByteSize() is deprecated and replaced by ByteSizeLong() since # protobuf 3.1. if(Protobuf_VERSION GREATER_EQUAL "3.1.0") target_compile_definitions(bitcoin-qt-base PRIVATE USE_PROTOBUF_MESSAGE_BYTESIZELONG) endif() # OpenSSL functionality include(BrewHelper) find_brew_prefix(OPENSSL_ROOT_DIR openssl) find_package(OpenSSL REQUIRED) include(CheckSymbolExists) set(CMAKE_REQUIRED_INCLUDES ${OPENSSL_INCLUDE_DIR}) set(CMAKE_REQUIRED_LIBRARIES ${OPENSSL_CRYPTO_LIBRARY}) check_symbol_exists(EVP_MD_CTX_new "openssl/evp.h" HAVE_DECL_EVP_MD_CTX_NEW) if(HAVE_DECL_EVP_MD_CTX_NEW) target_compile_definitions(bitcoin-qt-base PRIVATE HAVE_DECL_EVP_MD_CTX_NEW=1) endif() target_link_libraries(bitcoin-qt-base OpenSSL::SSL bitcoin-qt-protobuf ) endif() # Wallet if(BUILD_BITCOIN_WALLET) # Automoc option. set(AUTOMOC_MOC_OPTIONS -DENABLE_WALLET=1) # Add wallet functionality to bitcoin-qt target_sources(bitcoin-qt-base PRIVATE addressbookpage.cpp addresstablemodel.cpp askpassphrasedialog.cpp coincontroldialog.cpp coincontroltreewidget.cpp createwalletdialog.cpp editaddressdialog.cpp openuridialog.cpp overviewpage.cpp paymentserver.cpp qrimagewidget.cpp receivecoinsdialog.cpp receiverequestdialog.cpp recentrequeststablemodel.cpp sendcoinsdialog.cpp sendcoinsentry.cpp signverifymessagedialog.cpp transactiondesc.cpp transactiondescdialog.cpp transactionfilterproxy.cpp transactionrecord.cpp transactiontablemodel.cpp transactionview.cpp walletcontroller.cpp walletframe.cpp walletmodel.cpp walletmodeltransaction.cpp walletview.cpp ) # Add BIP70 functionality to bitcoin-qt if(ENABLE_BIP70) target_sources(bitcoin-qt-base PRIVATE paymentrequestplus.cpp ) endif() target_link_libraries(bitcoin-qt-base wallet) if(ENABLE_QRCODE) find_package(QREncode REQUIRED) target_link_libraries(bitcoin-qt-base QREncode::qrencode) endif() endif() # The executable add_executable(bitcoin-qt WIN32 main.cpp) include(WindowsVersionInfo) generate_windows_version_info(bitcoin-qt DESCRIPTION "GUI node for Bitcoin" ICONS "res/icons/bitcoin.ico" "res/icons/bitcoin_testnet.ico" ) target_link_libraries(bitcoin-qt bitcoin-qt-base) include(BinaryTest) add_to_symbols_check(bitcoin-qt) add_to_security_check(bitcoin-qt) include(InstallationHelper) install_target(bitcoin-qt) install_manpages(bitcoin-qt) if(${CMAKE_SYSTEM_NAME} MATCHES "Darwin") set(BITCOINQT_BUNDLE_ICON "res/icons/bitcoin.icns") get_filename_component(BITCOINQT_BUNDLE_ICON_NAME "${BITCOINQT_BUNDLE_ICON}" NAME ) set(INFO_PLIST_STRINGS_FILE "Base.lproj/InfoPlist.strings") set(INFO_PLIST_STRINGS_PATH "${CMAKE_CURRENT_BINARY_DIR}/${INFO_PLIST_STRINGS_FILE}") file(WRITE "${INFO_PLIST_STRINGS_PATH}" "{ CFBundleDisplayName = \"${PACKAGE_NAME}\"; CFBundleName = \"${PACKAGE_NAME}\"; }" ) set(EMPTY_LPROJ_FILE "${CMAKE_CURRENT_BINARY_DIR}/empty.lproj") file(TOUCH "${EMPTY_LPROJ_FILE}") target_sources(bitcoin-qt PRIVATE "${BITCOINQT_BUNDLE_ICON}" "${INFO_PLIST_STRINGS_PATH}" "${EMPTY_LPROJ_FILE}" ) string(JOIN ";" BITCOINQT_BUNDLE_RESOURCES "${BITCOINQT_BUNDLE_ICON}" "${EMPTY_LPROJ_FILE}" ) set(BITCOIN_QT_OSX_BUNDLE_NAME "BitcoinABC-Qt") set_target_properties(bitcoin-qt PROPERTIES MACOSX_BUNDLE ON OUTPUT_NAME "${BITCOIN_QT_OSX_BUNDLE_NAME}" MACOSX_BUNDLE_INFO_PLIST "${CMAKE_SOURCE_DIR}/share/qt/Info.plist.cmake.in" MACOSX_BUNDLE_BUNDLE_NAME "${BITCOIN_QT_OSX_BUNDLE_NAME}" MACOSX_BUNDLE_BUNDLE_VERSION "${bitcoin-abc_VERSION}" MACOSX_BUNDLE_GUI_IDENTIFIER "org.bitcoinabc.${BITCOIN_QT_OSX_BUNDLE_NAME}" MACOSX_BUNDLE_ICON_FILE "${BITCOINQT_BUNDLE_ICON_NAME}" MACOSX_BUNDLE_INFO_STRING "${bitcoin-abc_VERSION}, Copyright © 2009-${COPYRIGHT_YEAR} ${COPYRIGHT_HOLDERS_FINAL}" MACOSX_BUNDLE_LONG_VERSION_STRING "${bitcoin-abc_VERSION}" MACOSX_BUNDLE_SHORT_VERSION_STRING "${bitcoin-abc_VERSION}" RESOURCE "${BITCOINQT_BUNDLE_RESOURCES}" ) # The InfoPlist.strings files should be located in a resource subdirectory. # This is not supported by the RESOURCE property and require the use of the # MACOSX_PACKAGE_LOCATION property instead. The RESOURCE documentation has # an example demonstrating this behavior (see the appres.txt file): # https://cmake.org/cmake/help/latest/prop_tgt/RESOURCE.html set_source_files_properties( "${INFO_PLIST_STRINGS_PATH}" PROPERTIES MACOSX_PACKAGE_LOCATION "Resources/${INFO_PLIST_STRINGS_FILE}" ) # Create a stripped version of the application bundle to be used in the DMG. # Since the LOCATION property and the BundleUtilities package are deprecated # by cmake, only generator expressions can be used to determine the path to # the bundle and its executable. However the generator expressions are # solved at build time, making them unusable to do path computation at # configuration time. # The paths here are then hard-coded, which is safe since the structure of # an application bundle is well-known and specified by Apple. Note that this # will only work for building MacOS application bundle as the IOS structure # is slightly different. set(STRIPPED_BUNDLE "${CMAKE_CURRENT_BINARY_DIR}/stripped/${BITCOIN_QT_OSX_BUNDLE_NAME}.app") add_custom_command( OUTPUT "${STRIPPED_BUNDLE}" COMMAND ${CMAKE_COMMAND} -E copy_directory "$" "${STRIPPED_BUNDLE}" COMMAND ${CMAKE_STRIP} -u -r "${STRIPPED_BUNDLE}/Contents/MacOS/${BITCOIN_QT_OSX_BUNDLE_NAME}" DEPENDS bitcoin-qt ) include(DoOrFail) find_program_or_fail(CMAKE_INSTALL_NAME_TOOL "install_name_tool") find_program_or_fail(CMAKE_OTOOL "otool") set(QT_BASE_TRANSLATIONS "ar" "bg" "ca" "cs" "da" "de" "es" "fa" "fi" "fr" "gd" "gl" "he" "hu" "it" "ja" "ko" "lt" "lv" "pl" "pt" "ru" "sk" "sl" "sv" "uk" "zh_CN" "zh_TW" ) string(JOIN "," QT_LOCALES ${QT_BASE_TRANSLATIONS}) get_target_property(QMAKE_EXECUTABLE Qt5::qmake IMPORTED_LOCATION) execute_process( COMMAND "${QMAKE_EXECUTABLE}" -query QT_INSTALL_TRANSLATIONS OUTPUT_VARIABLE QT_TRANSLATION_DIR OUTPUT_STRIP_TRAILING_WHITESPACE ) function(get_qt_translation_dir QT_TRANSLATION_DIR) foreach(_locale ${ARGN}) find_path(_qt_translation_dir "qt_${_locale}.qm" HINTS "${QT_TRANSLATION_DIR}" PATH_SUFFIXES "translations" ) # Ensure that all the translation files are found, and are located # in the same directory. if(NOT _qt_translation_dir OR (_qt_translation_dir_previous AND (NOT _qt_translation_dir_previous STREQUAL _qt_translation_dir))) return() endif() set(_qt_translation_dir_previous _qt_translation_dir) endforeach() set(QT_TRANSLATION_DIR ${_qt_translation_dir} PARENT_SCOPE) endfunction() get_qt_translation_dir(QT_TRANSLATION_DIR ${QT_BASE_TRANSLATIONS}) if(NOT QT_TRANSLATION_DIR) message(FATAL_ERROR "Qt translation files are not found") endif() set(MACDEPLOY_DIR "${CMAKE_SOURCE_DIR}/contrib/macdeploy") - set(MACDEPLOYQTPLUS "${MACDEPLOY_DIR}/macdeployqtplus") + set(MACDEPLOYQTPLUS "${MACDEPLOY_DIR}/macdeployqtplus.py") set(DMG_DIST "${CMAKE_BINARY_DIR}/dist") add_custom_command( OUTPUT "${DMG_DIST}" COMMAND "INSTALLNAMETOOL=${CMAKE_INSTALL_NAME_TOOL}" "OTOOL=${CMAKE_OTOOL}" "STRIP=${CMAKE_STRIP}" "${Python_EXECUTABLE}" "${MACDEPLOYQTPLUS}" "${STRIPPED_BUNDLE}" -translations-dir "${QT_TRANSLATION_DIR}" -add-qt-tr "${QT_LOCALES}" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" DEPENDS "${STRIPPED_BUNDLE}" ) # Building the DMG background image requires several steps: # 1/ The SVG file must be edited to display the package name # 2/ The SVG file should be transformed into a couple PNG files, on for # low resolution screens and one for high resolution screens. # 3/ The PNG files must be transformed into a multi-resolution TIFF file. # The names are not set arbitrarily, they follow Apple's guidelines for # resolution independent bitmap images (see `man tiffutil`). set(BACKGROUND_SVG "background.svg") configure_file( "${CMAKE_SOURCE_DIR}/contrib/macdeploy/background.svg.cmake.in" "${BACKGROUND_SVG}" ) include(ImageHelper) set(BACKGROUND_PNG_LOWRES "${CMAKE_CURRENT_BINARY_DIR}/background_temp.png") set(BACKGROUND_PNG_HIRES "${CMAKE_CURRENT_BINARY_DIR}/background_temp@2x.png") set(BACKGROUND_TIFF_LOWRES "${CMAKE_CURRENT_BINARY_DIR}/background_temp.tiff") set(BACKGROUND_TIFF_HIRES "${CMAKE_CURRENT_BINARY_DIR}/background_temp@2x.tiff") set(BACKGROUND_TIFF_NAME "background.tiff") set(BACKGROUND_TIFF_MULTIRES "${CMAKE_BINARY_DIR}/${BACKGROUND_TIFF_NAME}") convert_svg_to_png("${BACKGROUND_SVG}" "${BACKGROUND_PNG_LOWRES}" 36) convert_svg_to_png("${BACKGROUND_SVG}" "${BACKGROUND_PNG_HIRES}" 72) convert_png_to_tiff("${BACKGROUND_PNG_LOWRES}" "${BACKGROUND_TIFF_LOWRES}") convert_png_to_tiff("${BACKGROUND_PNG_HIRES}" "${BACKGROUND_TIFF_HIRES}") cat_multi_resolution_tiff("${BACKGROUND_TIFF_MULTIRES}" "${BACKGROUND_TIFF_LOWRES}" "${BACKGROUND_TIFF_HIRES}") set(BACKGROUND_DIST_DIR "${DMG_DIST}/.background") set(BACKGROUND_DIST_TIFF "${BACKGROUND_DIST_DIR}/${BACKGROUND_TIFF_NAME}") add_custom_command( OUTPUT "${BACKGROUND_DIST_TIFF}" COMMAND ${CMAKE_COMMAND} -E make_directory "${BACKGROUND_DIST_DIR}" COMMAND ${CMAKE_COMMAND} -E copy "${BACKGROUND_TIFF_MULTIRES}" "${BACKGROUND_DIST_TIFF}" DEPENDS "${BACKGROUND_TIFF_MULTIRES}" "${DMG_DIST}" ) string(REPLACE " " "-" OSX_VOLNAME "${PACKAGE_NAME}") file(WRITE "${CMAKE_BINARY_DIR}/osx_volname" "${OSX_VOLNAME}") set(DMG_DSSTORE "${DMG_DIST}/.DS_Store") set(GEN_DSSTORE "${MACDEPLOY_DIR}/custom_dsstore.py") add_custom_command( OUTPUT "${DMG_DSSTORE}" COMMAND "${Python_EXECUTABLE}" "${GEN_DSSTORE}" "${DMG_DSSTORE}" "${OSX_VOLNAME}" DEPENDS "${GEN_DSSTORE}" "${DMG_DIST}" ) set(OSX_APPLICATION_DIR "Applications") set(OSX_APPLICATION_SYMLINK "${DMG_DIST}/${OSX_APPLICATION_DIR}") add_custom_command( OUTPUT "${OSX_APPLICATION_SYMLINK}" COMMAND ${CMAKE_COMMAND} -E create_symlink "/${OSX_APPLICATION_DIR}" "${OSX_APPLICATION_SYMLINK}" DEPENDS "${DMG_DIST}" ) add_custom_target(osx-deploydir DEPENDS "${OSX_APPLICATION_SYMLINK}" "${DMG_DSSTORE}" "${BACKGROUND_DIST_TIFF}" ) if(CMAKE_CROSSCOMPILING) find_program_or_fail(GENISOIMAGE_EXECUTABLE genisoimage) add_custom_target(osx-dmg COMMAND "${GENISOIMAGE_EXECUTABLE}" -no-cache-inodes -D -l -probe -V "${OSX_VOLNAME}" -no-pad -r -dir-mode 0755 -apple -o "${OSX_VOLNAME}.dmg" "${DMG_DIST}" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" ) add_dependencies(osx-dmg osx-deploydir) else() add_custom_target(osx-dmg COMMAND "${Python_EXECUTABLE}" "${MACDEPLOYQTPLUS}" "${STRIPPED_BUNDLE}" -translations-dir "${QT_TRANSLATION_DIR}" -add-qt-tr "${QT_LOCALES}" -dmg -fancy "${MACDEPLOY_DIR}/fancy.plist" -volname "${OSX_VOLNAME}" WORKING_DIRECTORY "${CMAKE_BINARY_DIR}" DEPENDS "${STRIPPED_BUNDLE}" "${BACKGROUND_TIFF_MULTIRES}" ) endif() endif() configure_file( "${CMAKE_SOURCE_DIR}/cmake/utils/translate.sh.in" "${CMAKE_CURRENT_BINARY_DIR}/translate.sh" @ONLY ) add_custom_target(translate COMMENT "Updating the translations..." COMMAND "${CMAKE_CURRENT_BINARY_DIR}/translate.sh" DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/translate.sh" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/.." ) # Test tests add_subdirectory(test)