From afe1de78379c8b4c12d86096ee658d13dc580334 Mon Sep 17 00:00:00 2001 From: Dan Rosser Date: Mon, 2 Nov 2026 00:11:41 +1100 Subject: [PATCH 1/2] Bundle ID: --bundle-id and a GUI field, kept in config.make APP_BUNDLE_ID and set in Xcode, Project.xcconfig, Info.plist, CMake and Android on every update --- commandLine/readme.md | 7 ++ commandLine/src/main.cpp | 18 +++- commandLine/src/projects/baseProject.cpp | 122 ++++++++++++++++++++++ commandLine/src/projects/baseProject.h | 5 + commandLine/src/projects/xcodeProject.cpp | 3 +- frontend/app.js | 11 +- frontend/index.html | 10 +- frontend/index.js | 44 +++++++- frontend/locales/de.json | 3 + frontend/locales/en.json | 3 + frontend/locales/es.json | 3 + frontend/locales/fr.json | 3 + frontend/locales/ja.json | 3 + frontend/locales/ko.json | 3 + frontend/locales/pt.json | 3 + frontend/locales/ru.json | 3 + frontend/locales/zh-CN.json | 3 + scripts/test_cmdline_args.sh | 26 +++++ 18 files changed, 267 insertions(+), 6 deletions(-) diff --git a/commandLine/readme.md b/commandLine/readme.md index d6daf837..c5ef62e7 100644 --- a/commandLine/readme.md +++ b/commandLine/readme.md @@ -83,6 +83,13 @@ The GUI shows them as System frameworks / System libraries with Advanced options +## Bundle ID + + ./projectGenerator -o"pathToOF" --bundle-id=com.example.myApp pathToProject + +Saved in `config.make` as `APP_BUNDLE_ID = com.example.myApp` and set on every update in the Xcode targets (`PRODUCT_BUNDLE_IDENTIFIER`), `Project.xcconfig`, a literal `CFBundleIdentifier` in a custom Info.plist, CMake `set(APP_BUNDLE_ID ...)` and the Android `applicationId` (`-` becomes `_` there). Without `--bundle-id` the project's own is kept: `config.make`, then `CMakeLists.txt`, the Xcode target, `Project.xcconfig`, `build.gradle`. The template's `cc.openFrameworks.` is the default. + + ## Environment variable If you want to skip using `-o` (for specifying the path to openframeworks) you can set an environment variable "PG_OF_PATH". For example, on OSX, you can set this in your `~/.bash_profile`: diff --git a/commandLine/src/main.cpp b/commandLine/src/main.cpp index aca8fc39..fe74e36b 100644 --- a/commandLine/src/main.cpp +++ b/commandLine/src/main.cpp @@ -39,7 +39,8 @@ enum optionIndex { UNKNOWN, MSYS2ENV, EMSCRIPTEN_WASM, APP_VERSION, - APP_BUILD + APP_BUILD, + BUNDLE_ID }; constexpr option::Descriptor usage[] = { @@ -74,6 +75,7 @@ constexpr option::Descriptor usage[] = { { APP_VERSION, 0, "", "app-version", option::Arg::Optional, " --app-version \tapp version, eg 1.2.3 (default 1.0): config.make / CMake APP_VERSION, Xcode MARKETING_VERSION, Android versionName" }, { APP_BUILD, 0, "", "app-build", option::Arg::Optional, " --app-build \tbuild number, eg 42 (default 1): config.make / CMake APP_BUILD, Xcode CURRENT_PROJECT_VERSION, Android versionCode" }, + { BUNDLE_ID, 0, "", "bundle-id", option::Arg::Optional, " --bundle-id \tbundle id, eg com.example.myApp (default cc.openFrameworks.): config.make / CMake APP_BUNDLE_ID, Xcode PRODUCT_BUNDLE_IDENTIFIER, Android applicationId" }, { 0, 0, 0, 0, 0, 0 } }; @@ -108,6 +110,7 @@ string postBuildScript; string emscriptenWasm; // empty keeps the project's config.make value string appVersion; // empty keeps the project's value string appBuild; +string appBundleId; // empty keeps the project's value string ofPathEnv; string templateName; string msys2Env = "ucrt64"; @@ -281,6 +284,7 @@ void updateProject(const fs::path & path, const string & target, bool bConsiderP project->setMsys2Environment(msys2Env); project->setEmscriptenWasm(emscriptenWasm); project->setAppVersion(appVersion, appBuild); + project->setAppBundleId(appBundleId); project->setSystemFrameworks(frameworks, bFrameworksPassedIn, systemLibs, bSystemLibsPassedIn); project->create(path, templateName); @@ -476,7 +480,7 @@ int pgMain(int argc, char ** argv) { // optionparser only takes attached values (-posx, --platforms=osx); join -p osx / --platforms osx, // but leave the last argument as the project path (-f is an empty -f) static const std::string valueShort { "paotsmfDPkW" }; - static const std::vector valueLong { "platforms", "addons", "ofPath", "template", "source", "ofmenu", "frameworks", "system-libs", "defines", "postbuild", "msys2env", "emscripten-wasm", "app-version", "app-build" }; + static const std::vector valueLong { "platforms", "addons", "ofPath", "template", "source", "ofmenu", "frameworks", "system-libs", "defines", "postbuild", "msys2env", "emscripten-wasm", "app-version", "app-build", "bundle-id" }; std::vector joinedArgs; for (int i = 0; i < argc; i++) { std::string a { argv[i] }; @@ -709,6 +713,15 @@ int pgMain(int argc, char ** argv) { } } + if (options[BUNDLE_ID].count() > 0 && options[BUNDLE_ID].arg != NULL) { + std::string id = ofTrim(options[BUNDLE_ID].arg); + if (std::regex_match(id, std::regex(R"([A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+)"))) { + appBundleId = id; + } else { + ofLogWarning() << "bundle id " << id << " should be letters, digits, - and dots (eg com.example.myApp) - keeping the project's bundle id"; + } + } + if (options[POSTBUILD].count() > 0 && options[POSTBUILD].arg != NULL) { postBuildScript = ofTrim(options[POSTBUILD].arg); } @@ -866,6 +879,7 @@ int pgMain(int argc, char ** argv) { project->setMsys2Environment(msys2Env); project->setEmscriptenWasm(emscriptenWasm); project->setAppVersion(appVersion, appBuild); + project->setAppBundleId(appBundleId); project->setSystemFrameworks(frameworks, bFrameworksPassedIn, systemLibs, bSystemLibsPassedIn); project->create(projectPath, templateName); if(bAddonsPassedIn){ diff --git a/commandLine/src/projects/baseProject.cpp b/commandLine/src/projects/baseProject.cpp index d858f8e3..4cd33645 100644 --- a/commandLine/src/projects/baseProject.cpp +++ b/commandLine/src/projects/baseProject.cpp @@ -246,6 +246,59 @@ static std::pair readGradleAppVersion(const fs::path & return { version, build }; } +// APP_BUNDLE_ID in config.make +static std::string readMakeBundleId(const fs::path & configMake) { + auto values = readMakeList(configMake, "APP_BUNDLE_ID"); + return values.empty() ? "" : values.back(); +} + +// set(APP_BUNDLE_ID ...) in an openFrameworks CMakeLists.txt +static std::string readCMakeBundleId(const fs::path & cmakeLists) { + static const std::regex bundleIdSet(R"re(^\s*set\(\s*APP_BUNDLE_ID\s+"?([^\s")]+)"?\s*\))re"); + std::string bundleId; + std::smatch m; + if (!fs::exists(cmakeLists)) return bundleId; + for (auto & line : fileToStrings(cmakeLists)) { + if (std::regex_search(line, m, bundleIdSet)) bundleId = m[1]; + } + return bundleId; +} + +// PRODUCT_BUNDLE_IDENTIFIER in the .xcodeproj target settings or Project.xcconfig (first one) +static std::string readXcodeBundleId(const fs::path & file) { + static const std::regex bundleIdSet(R"re(^\s*"?PRODUCT_BUNDLE_IDENTIFIER"?\s*[:=]\s*"?([^\s";]+)"?)re"); + std::smatch m; + if (!fs::exists(file)) return ""; + for (auto & line : fileToStrings(file)) { + if (std::regex_search(line, m, bundleIdSet)) return m[1]; + } + return ""; +} + +// applicationId in the android ofApp/build.gradle +static std::string readGradleBundleId(const fs::path & gradle) { + static const std::regex bundleIdSet(R"re(^\s*applicationId\s+['"]([^'"]+)['"])re"); + std::smatch m; + if (!fs::exists(gradle)) return ""; + for (auto & line : fileToStrings(gradle)) { + if (std::regex_search(line, m, bundleIdSet)) return m[1]; + } + return ""; +} + +// the templates' own ids: cc.openFrameworks.$(TARGET_NAME), com.openFrameworks.emptyExample, cc.openframeworks. +static bool isTemplateBundleId(const std::string & bundleId, const std::string & projectName) { + if (bundleId.empty() || bundleId.find('$') != std::string::npos) return true; + auto lower = ofToLower(bundleId); + auto name = ofToLower(projectName); + for (std::string prefix : { "cc.openframeworks.", "com.openframeworks." }) { + if (lower.rfind(prefix, 0) != 0) continue; + auto rest = lower.substr(prefix.size()); + if (rest == "emptyexample" || rest == "androidassimpexample" || rest == name || rest == name + "debug") return true; + } + return false; +} + bool baseProject::create(const fs::path & _path, string templateName){ // alert("baseProject::create " + path.string() + " : " + templateName, 35); auto path = _path; // just because it is const @@ -334,6 +387,29 @@ bool baseProject::create(const fs::path & _path, string templateName){ appBuild = !appBuildOption.empty() ? appBuildOption : (!build.empty() ? build : "1"); ofLogNotice() << "{ \"appVersion\": \"" << appVersion << "\", \"appBuild\": \"" << appBuild << "\" }"; } + + // bundle id: the CLI, then config.make, CMakeLists, Xcode target, Project.xcconfig, Android; the templates' default ids don't count + { + std::vector> found { + { "config.make", readMakeBundleId(projectDir / "config.make") }, + { "CMakeLists.txt", readCMakeBundleId(projectDir / "CMakeLists.txt") }, + { "xcodeproj", readXcodeBundleId(projectDir / (projectName + ".xcodeproj") / "project.pbxproj") }, + { "Project.xcconfig", readXcodeBundleId(projectDir / "Project.xcconfig") }, + { "build.gradle", readGradleBundleId(projectDir / "ofApp" / "build.gradle") }, + }; + std::string bundleId, bundleIdFrom; + for (auto & f : found) { + if (isTemplateBundleId(f.second, projectName)) continue; + if (bundleId.empty()) { + bundleId = f.second; + bundleIdFrom = f.first; + } else if (f.second != bundleId && ofToLower(f.second) != ofToLower(ofJoinString(ofSplitString(bundleId, "-"), "_"))) { + ofLogWarning() << "bundle id " << f.second << " in " << f.first << " differs from " << bundleId << " in " << bundleIdFrom << " - using " << bundleIdFrom; + } + } + appBundleId = !appBundleIdOption.empty() ? appBundleIdOption : bundleId; + if (!appBundleId.empty()) ofLogNotice() << "{ \"appBundleId\": \"" << appBundleId << "\" }"; + } if (emscriptenWasm == "64") { bool bHasWasm64Libs = false; if (fs::exists(getOFRoot() / "libs")) { @@ -486,6 +562,7 @@ bool baseProject::save(){ bool bWroteAppBuild = false; bool bWroteFrameworks = false; bool bWroteSystemLibs = false; + bool bWroteBundleId = false; auto isMakeVar = [](const std::string & line, const std::string & name) { auto config = ofTrim(line); if (config.rfind("#", 0) == 0) config = ofTrim(config.substr(1)); @@ -557,6 +634,9 @@ bool baseProject::save(){ else if( isMakeVar(str, "PROJECT_SYSTEM_LIBS") ){ writeMakeList(str, "PROJECT_SYSTEM_LIBS", systemLibs, bWroteSystemLibs); } + else if( isMakeVar(str, "APP_BUNDLE_ID") ){ + writeMakeList(str, "APP_BUNDLE_ID", appBundleId.empty() ? std::vector{} : std::vector{ appBundleId }, bWroteBundleId); + } else if( emscriptenWasm == "32" && str.rfind("EMSCRIPTEN_MEMORY64", 0) == 0 ){ saveConfig << "# " << str << std::endl; } else { @@ -581,9 +661,15 @@ bool baseProject::save(){ if (systemFrameworks.size() && !bWroteFrameworks) saveConfig << "PROJECT_FRAMEWORKS = " << ofJoinString(systemFrameworks, " ") << std::endl; if (systemLibs.size() && !bWroteSystemLibs) saveConfig << "PROJECT_SYSTEM_LIBS = " << ofJoinString(systemLibs, " ") << std::endl; } + if (!appBundleId.empty() && !bWroteBundleId) { + if (lines.size()) saveConfig << std::endl; + saveConfig << "# bundle id (CFBundleIdentifier, Android applicationId)" << std::endl; + saveConfig << "APP_BUNDLE_ID = " << appBundleId << std::endl; + } saveConfig.close(); saveCMakeLists(); saveAppVersionFiles(); + saveAppBundleIdFiles(); #ifdef OFADDON_OUTPUT_JSON_DEBUG saveAddonsToJson(); @@ -1237,6 +1323,31 @@ void baseProject::saveAppVersionFiles() { }); } +void baseProject::saveAppBundleIdFiles() { + if (appBundleId.empty()) return; + // target settings override this, but a reader of Project.xcconfig sees the real id + replaceLines(projectDir / "Project.xcconfig", { + { std::regex(R"re(^(\s*PRODUCT_BUNDLE_IDENTIFIER\s*=\s*)\S+)re"), appBundleId }, + }); + // Android application ids can't have '-' + replaceLines(projectDir / "ofApp" / "build.gradle", { + { std::regex(R"re(^(\s*applicationId\s+)['"][^'"]*['"])re"), "\"" + ofJoinString(ofSplitString(appBundleId, "-"), "_") + "\"" }, + }); + // a custom Info.plist with a literal id instead of $(PRODUCT_BUNDLE_IDENTIFIER) + static const std::regex plistId(R"re((CFBundleIdentifier\s*)([^<$]*)())re"); + for (auto & name : { "openFrameworks-Info.plist", "ofxiOS-Info.plist", "ofxtvOS-Info.plist" }) { + fs::path plist { projectDir / name }; + if (!fs::exists(plist)) continue; + std::ifstream in(plist); + std::string text((std::istreambuf_iterator(in)), std::istreambuf_iterator()); + in.close(); + std::smatch m; + if (!std::regex_search(text, m, plistId) || m[2].str() == appBundleId) continue; + std::ofstream out(plist); + out << m.prefix().str() << m[1].str() << appBundleId << m[3].str() << m.suffix().str(); + } +} + // OF_ROOT and PROJECT_EMSCRIPTEN_WASM in an openFrameworks app CMakeLists.txt, like config.make void baseProject::saveCMakeLists() { fs::path cmakeLists { projectDir / "CMakeLists.txt" }; @@ -1253,6 +1364,8 @@ void baseProject::saveCMakeLists() { static const std::regex ofRootSet(R"re(^(\s*)set\(OF_ROOT\s+"[^"]*"\))re"); static const std::regex wasmSet(R"re(^\s*set\(\s*PROJECT_EMSCRIPTEN_WASM\b)re"); static const std::regex appVersionSet(R"re(^\s*#?\s*set\(\s*APP_(VERSION|BUILD)\b)re"); + static const std::regex bundleIdSet(R"re(^\s*#?\s*set\(\s*APP_BUNDLE_ID\b)re"); + bool bWroteBundleId = appBundleId.empty(); static const std::regex addApp(R"re(^\s*of_add_app\s*\()re"); string comment = isDefaultAppVersion() ? "# " : ""; bool bWroteAppVersion = false; @@ -1269,10 +1382,19 @@ void baseProject::saveCMakeLists() { writeAppVersion(out); continue; } + if (!appBundleId.empty() && std::regex_search(line, bundleIdSet)) { + if (!bWroteBundleId) out << "set(APP_BUNDLE_ID " << appBundleId << ")" << std::endl; + bWroteBundleId = true; + continue; + } if (std::regex_search(line, addApp) && !bWroteAppVersion) { if (!isDefaultAppVersion()) writeAppVersion(out); bWroteAppVersion = true; } + if (std::regex_search(line, addApp) && !bWroteBundleId) { + out << "set(APP_BUNDLE_ID " << appBundleId << ")" << std::endl; + bWroteBundleId = true; + } if (!emscriptenWasm.empty() && std::regex_search(line, wasmSet)) continue; if (line.find(OF_CMAKE_INCLUDE) != std::string::npos && emscriptenWasm == "64") { out << "# emscripten: 32 (default) or 64 (wasm64, MEMORY64)" << std::endl; diff --git a/commandLine/src/projects/baseProject.h b/commandLine/src/projects/baseProject.h index 16e4b07b..ec7206c4 100644 --- a/commandLine/src/projects/baseProject.h +++ b/commandLine/src/projects/baseProject.h @@ -119,6 +119,8 @@ class baseProject { // emscripten wasm32 / wasm64 (-W/--emscripten-wasm), written to config.make PROJECT_EMSCRIPTEN_WASM; empty keeps the project's value void setEmscriptenWasm(const std::string & bits) { emscriptenWasmOption = bits; } void setAppVersion(const std::string & version, const std::string & build) { appVersionOption = version; appBuildOption = build; } + // bundle id (--bundle-id), written to config.make APP_BUNDLE_ID; empty keeps the project's (default cc.openFrameworks.) + void setAppBundleId(const std::string & bundleId) { appBundleIdOption = bundleId; } #ifdef OFADDON_OUTPUT_JSON_DEBUG @@ -201,6 +203,9 @@ class baseProject { std::string appVersionOption, appBuildOption; // from the CLI, empty = keep std::string appVersion { "1.0" }, appBuild { "1" }; // value for this project bool isDefaultAppVersion() const { return appVersion == "1.0" && appBuild == "1"; } + std::string appBundleIdOption; // from the CLI, empty = keep + std::string appBundleId; // value for this project, empty = the template's cc.openFrameworks. + void saveAppBundleIdFiles(); //cached addons - if an addon is requested more than once, avoid loading from disk as it's quite slow std::map> addonsCache; //indexed by [platform][supplied path] diff --git a/commandLine/src/projects/xcodeProject.cpp b/commandLine/src/projects/xcodeProject.cpp index 4a413f49..53d53dc1 100644 --- a/commandLine/src/projects/xcodeProject.cpp +++ b/commandLine/src/projects/xcodeProject.cpp @@ -1142,7 +1142,7 @@ bool xcodeProject::saveProjectFile() { } } - // app version / build on every target configuration (target settings override Project.xcconfig) + // app version / build / bundle id on every target configuration (target settings override Project.xcconfig) try { auto & objects = j["objects"]; for (auto & target : objects) { @@ -1153,6 +1153,7 @@ bool xcodeProject::saveProjectFile() { auto & config = objects[configUUID.get()]; config["buildSettings"]["MARKETING_VERSION"] = appVersion; config["buildSettings"]["CURRENT_PROJECT_VERSION"] = appBuild; + if (!appBundleId.empty()) config["buildSettings"]["PRODUCT_BUNDLE_IDENTIFIER"] = appBundleId; } } } catch (std::exception & e) { diff --git a/frontend/app.js b/frontend/app.js index e8c7637c..ee1aa997 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -218,6 +218,7 @@ ipcRenderer.on('setGenerateMode', (event, arg) => { $('#appVersion').val(''); $('#appBuild').val(''); $('#appVersionNote').hide(); + $('#bundleId').val(''); $('#systemFrameworks').val(''); $('#systemLibs').val(''); } @@ -225,9 +226,10 @@ ipcRenderer.on('setGenerateMode', (event, arg) => { //------------------------------------------- // existing project's app version / build, so updating keeps them -ipcRenderer.on('setAppVersion', (event, { version, build, differs }) => { +ipcRenderer.on('setAppVersion', (event, { version, build, differs, bundleId }) => { $('#appVersion').val(version || ''); $('#appBuild').val(build || ''); + $('#bundleId').val(bundleId || ''); const note = (differs || []).map((d) => t('note.appVersionDiffers', { file: d.file, value: d.value })).join(' '); $('#appVersionNote').text(note).toggle(!!defaultSettings.advancedMode && note.length > 0); }); @@ -1207,6 +1209,9 @@ function saveDefaultSettings() { } //---------------------------------------- +// letters, digits, - and dots, at least two parts (same check as the commandLine) +const BUNDLE_ID_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/; + function generate() { // let's get all the info: const platformValueArray = getPlatformList(); @@ -1287,6 +1292,7 @@ function generate() { emscriptenWasm: $('#wasmField').css('display') !== 'none' ? $('#wasmDropdown').dropdown('get value') : '', appVersion: $('#appVersion').val().trim(), appBuild: $('#appBuild').val().trim(), + appBundleId: $('#bundleId').val().trim(), // only for Apple targets; hidden = keep the project's config.make values systemFrameworks: $('#systemFrameworksField').css('display') !== 'none' ? $('#systemFrameworks').val().split(/[\s,]+/).filter(Boolean) : null, systemLibs: $('#systemFrameworksField').css('display') !== 'none' ? $('#systemLibs').val().split(/[\s,]+/).filter(Boolean) : null @@ -1303,6 +1309,8 @@ function generate() { $("#appVersion").oneTimeTooltip(t('tooltip.appVersionInvalid')); } else if (gen.appBuild !== '' && !/^[1-9]\d*$/.test(gen.appBuild)) { $("#appBuild").oneTimeTooltip(t('tooltip.appBuildInvalid')); + } else if (gen.appBundleId !== '' && !BUNDLE_ID_RE.test(gen.appBundleId)) { + $("#bundleId").oneTimeTooltip(t('tooltip.bundleIdInvalid')); } else { // the template pick is cleared in the #projectName change handler instead, // as soon as the user points at a different project - not here, so a plain @@ -1481,6 +1489,7 @@ function updateRendererFieldVisibility() { $('#glesField').toggle(!!defaultSettings.advancedMode && selectedPlatforms.some((p) => ['ios', 'macos'].includes(p))); // advanced only; hidden fields keep the project's values, so updating still keeps them $('#appVersionField').css('display', defaultSettings.advancedMode ? 'flex' : 'none'); + $('#bundleIdField').toggle(!!defaultSettings.advancedMode); $('#appVersionNote').toggle(!!defaultSettings.advancedMode && $('#appVersionNote').text().length > 0); $('#systemFrameworksField').css('display', defaultSettings.advancedMode && selectedPlatforms.some((p) => ['osx', 'ios', 'macos', 'tvos'].includes(p)) ? 'flex' : 'none'); diff --git a/frontend/index.html b/frontend/index.html index f81e35be..5bcbb996 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -318,6 +318,14 @@ +