| @@ -34,24 +34,130 @@ constexpr auto* macOSArch_32BitUniversal = "32BitUniversal"; | |||||
| constexpr auto* macOSArch_64BitUniversal = "64BitUniversal"; | constexpr auto* macOSArch_64BitUniversal = "64BitUniversal"; | ||||
| constexpr auto* macOSArch_64Bit = "64BitIntel"; | constexpr auto* macOSArch_64Bit = "64BitIntel"; | ||||
| static constexpr const char* configGuardTemplate = R"( | |||||
| if test "$CONFIGURATION" = "$JUCE_CONFIG_NAME"; then : | |||||
| $JUCE_GUARDED_SCRIPT | |||||
| fi | |||||
| )"; | |||||
| static constexpr const char* copyPluginScriptTemplate = R"( | |||||
| if [ -e "$JUCE_INSTALL_PATH$JUCE_PRODUCT_NAME" ]; then : | |||||
| echo "Destination '$JUCE_INSTALL_PATH$JUCE_PRODUCT_NAME' exists, overwriting" | |||||
| rm -rf "$JUCE_INSTALL_PATH$JUCE_PRODUCT_NAME" | |||||
| fi | |||||
| mkdir -p "$JUCE_INSTALL_PATH" | |||||
| ln -sfhv "$JUCE_SOURCE_BUNDLE" "$JUCE_INSTALL_PATH" | |||||
| )"; | |||||
| static constexpr const char* adhocCodeSignTemplate = R"( | |||||
| xcrun codesign --verify "$JUCE_FULL_PRODUCT_PATH" || xcrun codesign -f -s - "$JUCE_FULL_PRODUCT_PATH" | |||||
| )"; | |||||
| //============================================================================== | |||||
| inline String doubleQuoted (const String& text) | |||||
| { | |||||
| return text.quoted(); | |||||
| } | |||||
| inline String singleQuoted (const String& text) | |||||
| { | |||||
| return text.quoted ('\''); | |||||
| } | |||||
| //============================================================================== | |||||
| class ScriptBuilder | |||||
| { | |||||
| public: | |||||
| //============================================================================== | |||||
| ScriptBuilder() = default; | |||||
| explicit ScriptBuilder (int indentIn) : indent (indentIn) {} | |||||
| //============================================================================== | |||||
| template <typename... Args> | |||||
| ScriptBuilder& run (const String& command, Args&&... args) | |||||
| { | |||||
| const auto joined = StringArray { command, std::forward<Args> (args)... }.joinIntoString (" "); | |||||
| return echo ("Running " + joined).insertLine (joined); | |||||
| } | |||||
| ScriptBuilder& echo (const String& text) | |||||
| { | |||||
| return insertLine ("echo " + doubleQuoted (text.removeCharacters ("\""))); | |||||
| } | |||||
| ScriptBuilder& remove (const String& path) | |||||
| { | |||||
| return run ("rm -rf", doubleQuoted (path)); | |||||
| } | |||||
| ScriptBuilder& copy (const String& src, const String& dst) | |||||
| { | |||||
| return run ("ditto", doubleQuoted (src), doubleQuoted (dst)); | |||||
| } | |||||
| ScriptBuilder& set (const String& variableName, const String& defaultValue = singleQuoted ("")) | |||||
| { | |||||
| return insertLine (variableName + "=" + doubleQuoted (defaultValue)); | |||||
| } | |||||
| //============================================================================== | |||||
| ScriptBuilder& ifThen (const String& condition, const String& then) | |||||
| { | |||||
| jassert (then.isNotEmpty()); | |||||
| return insertLine ("if [[ " + condition + " ]]; then") | |||||
| .insertScript (ScriptBuilder { indent + 1 }.insertScript (then).toString()) | |||||
| .insertLine ("fi") | |||||
| .insertLine(); | |||||
| } | |||||
| ScriptBuilder& ifCompare (const String& lhs, const String& rhs, const String& comparison, const String& then) | |||||
| { | |||||
| return ifThen (StringArray { doubleQuoted (lhs), comparison, doubleQuoted (rhs) }.joinIntoString (" "), then); | |||||
| } | |||||
| ScriptBuilder& ifEqual (const String& lhs, const String& rhs, const String& then) | |||||
| { | |||||
| return ifCompare (lhs, rhs, "==", then); | |||||
| } | |||||
| ScriptBuilder& ifSet (const String& variable, const String& then) | |||||
| { | |||||
| return ifThen ("-n " + doubleQuoted ("${" + variable + "-}"), then); | |||||
| } | |||||
| //============================================================================== | |||||
| ScriptBuilder& insertLine (const String& line = {}) | |||||
| { | |||||
| constexpr auto spacesPerIndent = 2; | |||||
| script.add ((String::repeatedString (" ", spacesPerIndent * indent) + line).trimEnd()); | |||||
| return *this; | |||||
| } | |||||
| ScriptBuilder& insertLines (const StringArray& lines) | |||||
| { | |||||
| for (const auto& line : lines) | |||||
| insertLine (line); | |||||
| return *this; | |||||
| } | |||||
| ScriptBuilder& insertScript (const String& s) | |||||
| { | |||||
| return insertLines (StringArray::fromLines (s.trimEnd())); | |||||
| } | |||||
| //============================================================================== | |||||
| bool isEmpty() const | |||||
| { | |||||
| return script.isEmpty(); | |||||
| } | |||||
| String toString() const | |||||
| { | |||||
| return script.joinIntoString ("\n") + "\n"; | |||||
| } | |||||
| String toStringWithShellOptions (const String& options) const | |||||
| { | |||||
| if (isEmpty()) | |||||
| return {}; | |||||
| return ScriptBuilder{}.insertLine ("set " + options) | |||||
| .insertLine() | |||||
| .insertScript (toString()) | |||||
| .toString(); | |||||
| } | |||||
| String toStringWithDefaultShellOptions() const | |||||
| { | |||||
| return toStringWithShellOptions ("-euo pipefail"); | |||||
| } | |||||
| private: | |||||
| StringArray script; | |||||
| int indent{}; | |||||
| }; | |||||
| //============================================================================== | //============================================================================== | ||||
| class XcodeProjectExporter final : public ProjectExporter, | class XcodeProjectExporter final : public ProjectExporter, | ||||
| @@ -1985,17 +2091,17 @@ public: | |||||
| //============================================================================== | //============================================================================== | ||||
| void addShellScriptBuildPhase (const String& phaseName, const String& script) | void addShellScriptBuildPhase (const String& phaseName, const String& script) | ||||
| { | { | ||||
| if (script.trim().isNotEmpty()) | |||||
| { | |||||
| auto v = addBuildPhase ("PBXShellScriptBuildPhase", {}); | |||||
| v.setProperty (Ids::name, phaseName, nullptr); | |||||
| v.setProperty ("alwaysOutOfDate", 1, nullptr); | |||||
| v.setProperty ("shellPath", "/bin/sh", nullptr); | |||||
| v.setProperty ("shellScript", script.replace ("\\", "\\\\") | |||||
| .replace ("\"", "\\\"") | |||||
| .replace ("\r\n", "\\n") | |||||
| .replace ("\n", "\\n"), nullptr); | |||||
| } | |||||
| if (script.trim().isEmpty()) | |||||
| return; | |||||
| auto v = addBuildPhase ("PBXShellScriptBuildPhase", {}); | |||||
| v.setProperty (Ids::name, phaseName, nullptr); | |||||
| v.setProperty ("alwaysOutOfDate", 1, nullptr); | |||||
| v.setProperty ("shellPath", "/bin/sh", nullptr); | |||||
| v.setProperty ("shellScript", script.replace ("\\", "\\\\") | |||||
| .replace ("\"", "\\\"") | |||||
| .replace ("\r\n", "\\n") | |||||
| .replace ("\n", "\\n"), nullptr); | |||||
| } | } | ||||
| void addCopyFilesPhase (const String& phaseName, const StringArray& files, XcodeCopyFilesDestinationIDs dst) | void addCopyFilesPhase (const String& phaseName, const StringArray& files, XcodeCopyFilesDestinationIDs dst) | ||||
| @@ -2419,38 +2525,30 @@ private: | |||||
| // When building LV2 and VST3 plugins on Arm macs, we need to load and run the plugin | // When building LV2 and VST3 plugins on Arm macs, we need to load and run the plugin | ||||
| // bundle during a post-build step in order to generate the plugin's supporting files. | // bundle during a post-build step in order to generate the plugin's supporting files. | ||||
| // Arm macs will only load shared libraries if they are signed, but Xcode runs its | // Arm macs will only load shared libraries if they are signed, but Xcode runs its | ||||
| // signing step after any post-build scripts. As a workaround, we check whether the | |||||
| // plugin is signed and generate an adhoc certificate if necessary, before running | |||||
| // the manifest-generator. | |||||
| // signing step after any post-build scripts. As a workaround, we sign the plugin | |||||
| // using an adhoc certificate. | |||||
| if (target->type == XcodeTarget::VST3PlugIn || target->type == XcodeTarget::LV2PlugIn) | if (target->type == XcodeTarget::VST3PlugIn || target->type == XcodeTarget::LV2PlugIn) | ||||
| { | { | ||||
| String script = "set -e\n"; | |||||
| // Delete manifest if it's left over from an old build | |||||
| if (target->type == XcodeTarget::VST3PlugIn) | |||||
| script << "rm -f \"$CONFIGURATION_BUILD_DIR/$FULL_PRODUCT_NAME/Contents/moduleinfo.json\"\n"; | |||||
| // Sign the bundle so that it can be loaded by the manifest generator tools | |||||
| script << String { adhocCodeSignTemplate }.replace ("$JUCE_FULL_PRODUCT_PATH", | |||||
| "$CONFIGURATION_BUILD_DIR/$FULL_PRODUCT_NAME"); | |||||
| auto script = ScriptBuilder{} | |||||
| .run ("codesign --verbose=4 --force --sign -", doubleQuoted ("${CONFIGURATION_BUILD_DIR}/${FULL_PRODUCT_NAME}")) | |||||
| .insertLine(); | |||||
| if (target->type == XcodeTarget::LV2PlugIn) | if (target->type == XcodeTarget::LV2PlugIn) | ||||
| { | { | ||||
| // Note: LV2 has a non-standard config build dir | // Note: LV2 has a non-standard config build dir | ||||
| script << "\"$CONFIGURATION_BUILD_DIR/../" | |||||
| + Project::getLV2FileWriterName() | |||||
| + "\" \"$CONFIGURATION_BUILD_DIR/$FULL_PRODUCT_NAME\"\n"; | |||||
| script.run (doubleQuoted ("${CONFIGURATION_BUILD_DIR}/../" + Project::getLV2FileWriterName()), | |||||
| doubleQuoted ("${CONFIGURATION_BUILD_DIR}/${FULL_PRODUCT_NAME}")); | |||||
| } | } | ||||
| else if (target->type == XcodeTarget::VST3PlugIn) | else if (target->type == XcodeTarget::VST3PlugIn) | ||||
| { | { | ||||
| script << "\"$CONFIGURATION_BUILD_DIR/" << Project::getVST3FileWriterName() << "\" " | |||||
| "-create " | |||||
| "-version " << project.getVersionString().quoted() << " " | |||||
| "-path \"$CONFIGURATION_BUILD_DIR/$FULL_PRODUCT_NAME\" " | |||||
| "-output \"$CONFIGURATION_BUILD_DIR/$FULL_PRODUCT_NAME/Contents/Resources/moduleinfo.json\"\n"; | |||||
| script.run (doubleQuoted ("${CONFIGURATION_BUILD_DIR}/" + Project::getVST3FileWriterName()), | |||||
| "-create", | |||||
| "-version", doubleQuoted (project.getVersionString()), | |||||
| "-path", doubleQuoted ("${CONFIGURATION_BUILD_DIR}/${FULL_PRODUCT_NAME}"), | |||||
| "-output", doubleQuoted ("${CONFIGURATION_BUILD_DIR}/${FULL_PRODUCT_NAME}/Contents/Resources/moduleinfo.json")); | |||||
| } | } | ||||
| target->addShellScriptBuildPhase ("Update manifest", script); | |||||
| target->addShellScriptBuildPhase ("Update manifest", script.toStringWithDefaultShellOptions()); | |||||
| } | } | ||||
| target->addShellScriptBuildPhase ("Post-build script", getPostBuildScript()); | target->addShellScriptBuildPhase ("Post-build script", getPostBuildScript()); | ||||
| @@ -2463,51 +2561,51 @@ private: | |||||
| && target->type == XcodeTarget::UnityPlugIn) | && target->type == XcodeTarget::UnityPlugIn) | ||||
| embedUnityScript(); | embedUnityScript(); | ||||
| StringArray copyPluginScript; | |||||
| ScriptBuilder copyPluginScript; | |||||
| for (ConstConfigIterator config (*this); config.next();) | for (ConstConfigIterator config (*this); config.next();) | ||||
| { | { | ||||
| auto& xcodeConfig = static_cast<const XcodeBuildConfiguration&> (*config); | auto& xcodeConfig = static_cast<const XcodeBuildConfiguration&> (*config); | ||||
| auto installPath = target->getInstallPathForConfiguration (xcodeConfig); | auto installPath = target->getInstallPathForConfiguration (xcodeConfig); | ||||
| if (target->xcodeCopyToProductInstallPathAfterBuild && installPath.isNotEmpty()) | |||||
| { | |||||
| if (installPath.startsWith ("~")) | |||||
| installPath = installPath.replace ("~", "$(HOME)"); | |||||
| installPath = installPath.replace ("$(HOME)", "$HOME"); | |||||
| if (installPath.isEmpty() || ! target->xcodeCopyToProductInstallPathAfterBuild) | |||||
| continue; | |||||
| const auto configGuard = String { configGuardTemplate }.replace ("$JUCE_CONFIG_NAME", | |||||
| config->getName()); | |||||
| if (installPath.startsWith ("~")) | |||||
| installPath = installPath.replace ("~", "$(HOME)"); | |||||
| const auto signScript = String { adhocCodeSignTemplate }.replace ("$JUCE_FULL_PRODUCT_PATH", | |||||
| "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}"); | |||||
| installPath = installPath.replace ("$(HOME)", "${HOME}"); | |||||
| const auto copyScript = [&] | |||||
| const auto copyScript = [&] | |||||
| { | |||||
| const auto generateCopyScript = [](String sourcePlugin, String destinationDir) | |||||
| { | { | ||||
| const auto base = String { copyPluginScriptTemplate } | |||||
| .replace ("$JUCE_CONFIG_NAME", config->getName()) | |||||
| .replace ("$JUCE_INSTALL_PATH", installPath); | |||||
| return ScriptBuilder{} | |||||
| .set ("destinationPlugin", destinationDir + "/$(basename " + doubleQuoted (sourcePlugin) + ")") | |||||
| .remove ("${destinationPlugin}") | |||||
| .copy (sourcePlugin, "${destinationPlugin}") | |||||
| .insertLine() | |||||
| .ifSet ("CODE_SIGN_ENTITLEMENTS", | |||||
| R"(entitlementsArg=(--entitlements "${CODE_SIGN_ENTITLEMENTS}"))") | |||||
| .run ("codesign --verbose=4 --force --sign", | |||||
| doubleQuoted ("${CODE_SIGN_IDENTITY:--}"), | |||||
| "${entitlementsArg[*]-}", | |||||
| "${OTHER_CODE_SIGN_ARGS-}", | |||||
| doubleQuoted ("${destinationPlugin}")); | |||||
| }; | |||||
| if (target->type == XcodeTarget::Target::LV2PlugIn) | |||||
| { | |||||
| return base.replace ("$JUCE_PRODUCT_NAME", "${TARGET_BUILD_DIR##*/}") | |||||
| .replace ("$JUCE_SOURCE_BUNDLE", "${TARGET_BUILD_DIR}"); | |||||
| } | |||||
| if (target->type == XcodeTarget::Target::LV2PlugIn) | |||||
| return generateCopyScript ("${TARGET_BUILD_DIR}", installPath); | |||||
| return base.replace ("$JUCE_PRODUCT_NAME", "${FULL_PRODUCT_NAME}") | |||||
| .replace ("$JUCE_SOURCE_BUNDLE", "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}"); | |||||
| }(); | |||||
| return generateCopyScript ("${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}", installPath); | |||||
| }(); | |||||
| copyPluginScript.add (configGuard.replace ("$JUCE_GUARDED_SCRIPT", signScript + copyScript)); | |||||
| } | |||||
| copyPluginScript.ifEqual (doubleQuoted ("${CONFIGURATION}"), doubleQuoted (config->getName()), | |||||
| copyScript.toString()); | |||||
| } | } | ||||
| if (! copyPluginScript.isEmpty()) | if (! copyPluginScript.isEmpty()) | ||||
| { | |||||
| copyPluginScript.insert (0, "set -e"); | |||||
| target->addShellScriptBuildPhase ("Plugin Copy Step", copyPluginScript.joinIntoString ("\n")); | |||||
| } | |||||
| target->addShellScriptBuildPhase ("Plugin Copy Step", copyPluginScript.toStringWithDefaultShellOptions()); | |||||
| addTargetObject (*target); | addTargetObject (*target); | ||||
| } | } | ||||