diff --git a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h index b8c436b9e6..044b35eb32 100644 --- a/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h +++ b/extras/Projucer/Source/ProjectSaving/jucer_ProjectExport_Xcode.h @@ -34,24 +34,130 @@ constexpr auto* macOSArch_32BitUniversal = "32BitUniversal"; constexpr auto* macOSArch_64BitUniversal = "64BitUniversal"; constexpr auto* macOSArch_64Bit = "64BitIntel"; -static constexpr const char* configGuardTemplate = R"( -if test "$CONFIGURATION" = "$JUCE_CONFIG_NAME"; then : -$JUCE_GUARDED_SCRIPT -fi -)"; +//============================================================================== +inline String doubleQuoted (const String& text) +{ + return text.quoted(); +} -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" -)"; +inline String singleQuoted (const String& text) +{ + return text.quoted ('\''); +} -static constexpr const char* adhocCodeSignTemplate = R"( -xcrun codesign --verify "$JUCE_FULL_PRODUCT_PATH" || xcrun codesign -f -s - "$JUCE_FULL_PRODUCT_PATH" -)"; +//============================================================================== +class ScriptBuilder +{ +public: + //============================================================================== + ScriptBuilder() = default; + explicit ScriptBuilder (int indentIn) : indent (indentIn) {} + + //============================================================================== + template + ScriptBuilder& run (const String& command, Args&&... args) + { + const auto joined = StringArray { command, std::forward (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, @@ -1985,17 +2091,17 @@ public: //============================================================================== 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) @@ -2419,38 +2525,30 @@ private: // 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. // 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) { - 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) { // 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) { - 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()); @@ -2463,51 +2561,51 @@ private: && target->type == XcodeTarget::UnityPlugIn) embedUnityScript(); - StringArray copyPluginScript; + ScriptBuilder copyPluginScript; for (ConstConfigIterator config (*this); config.next();) { auto& xcodeConfig = static_cast (*config); auto installPath = target->getInstallPathForConfiguration (xcodeConfig); - if (target->xcodeCopyToProductInstallPathAfterBuild && installPath.isNotEmpty()) + if (installPath.isEmpty() || ! target->xcodeCopyToProductInstallPathAfterBuild) + continue; + + if (installPath.startsWith ("~")) + installPath = installPath.replace ("~", "$(HOME)"); + + installPath = installPath.replace ("$(HOME)", "${HOME}"); + + const auto copyScript = [&] { - if (installPath.startsWith ("~")) - installPath = installPath.replace ("~", "$(HOME)"); - - installPath = installPath.replace ("$(HOME)", "$HOME"); - - const auto configGuard = String { configGuardTemplate }.replace ("$JUCE_CONFIG_NAME", - config->getName()); - - const auto signScript = String { adhocCodeSignTemplate }.replace ("$JUCE_FULL_PRODUCT_PATH", - "${TARGET_BUILD_DIR}/${FULL_PRODUCT_NAME}"); - - 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()) - { - copyPluginScript.insert (0, "set -e"); - target->addShellScriptBuildPhase ("Plugin Copy Step", copyPluginScript.joinIntoString ("\n")); - } + target->addShellScriptBuildPhase ("Plugin Copy Step", copyPluginScript.toStringWithDefaultShellOptions()); addTargetObject (*target); }