[iOS | flavors]: Fix sync flavors for Unity Project
Is your feature request related to a problem? Please describe. If you have a flavored app, till now App crashes on iOS, because flavors are not synced with Flutter Runner.
Describe the solution you'd like I automated the flavor creation (sync same config for nth-flavor) in Build.cs.
Additional context
First of all you need:
private static readonly string FlutterIOSPath = Path.GetFullPath(Path.Combine(ProjectPath, "../../ios"));
Solution
private static void ModifyXCodeBuild() {
var pbxprojFile = Path.Combine(IOSExportPath, "Unity-iPhone.xcodeproj/project.pbxproj");
var pbxprojText = File.ReadAllText(pbxprojFile);
/// Get Full Configuration for nth buildConfiguration
string pbxProjectFullBuildConfiguration = pbxprojText.Substring(pbxprojText.IndexOf("/* Begin XCBuildConfiguration section */")).GetUntilOrEmpty("/* End XCBuildConfiguration section */");
string newpbxProjectFullBuildConfiguration = pbxProjectFullBuildConfiguration;
/// Get buildConfigurations section from Unity-Project
var indexOfPBXProjectBuildConfigurationOfUnityIPhone = pbxprojText.IndexOf("/* Build configuration list for PBXProject \"Unity-iPhone\" */ = {");
var configurationsFromUnityProject = pbxprojText.Substring(indexOfPBXProjectBuildConfigurationOfUnityIPhone).GetUntilOrEmpty("};");
var buildConfigurations = configurationsFromUnityProject.Substring(configurationsFromUnityProject.IndexOf("buildConfigurations = (")).GetUntilOrEmpty(");").Replace("buildConfigurations = (", "");
string[] buildConfigurationItems = buildConfigurations.Substring(1).Split(",").Where(s => !s.Contains("ReleaseForProfiling") && !s.Contains("ReleaseForRunning")).ToArray();
/// Read existing flavors from iOS Flutter Project (except Runner Flavor itself)
string[] flutterFlavors = Directory.GetFiles($"{FlutterIOSPath}/Runner.xcodeproj/xcshareddata/xcschemes", "*.xcscheme").Where(s => !s.Contains("Runner.xcscheme")).ToArray();
/// If no flavors exists for this App skip flavor generation
if (flutterFlavors.Length == 0) return;
string[] flavorNames = new string[flutterFlavors.Length];
for (int i = 0; i < flutterFlavors.Length; i++) {
flavorNames[i] = Path.GetFileName(flutterFlavors[i]).Split(".")[0];
}
var newbuildConfigurations = buildConfigurations;
foreach (var configuration in buildConfigurationItems) {
if (configuration.Contains("/* ") && configuration.Contains(" */")) {
var configName = configuration.GetUntilOrEmpty(" */").Split(" ").Last();
foreach (var fl in flavorNames) {
var newXCodeId = GenerateXCodeId();
var oldId = configuration.Split(" ").First().Replace(" ", "");
var newName = configuration.Replace($"{configName}", $"{configName}-{fl}").Replace(oldId, newXCodeId);
newbuildConfigurations += $"{newName},\n";
var index = pbxProjectFullBuildConfiguration.IndexOf($"/* {configName} */");
var sameConfig = pbxProjectFullBuildConfiguration
.Substring(index)
.GetUntilOrEmpty($"name = {configName};");
string newstr = newXCodeId + " " + $"/* {configName}-{fl} */ = " + "{";
var firstLine = sameConfig.Split(new[] { '\r', '\n' }).FirstOrDefault();
sameConfig = sameConfig.Replace(firstLine, newstr);
sameConfig += $"name = {configName}-{fl};\n" + "};\n";
newpbxProjectFullBuildConfiguration += sameConfig;
}
}
}
newbuildConfigurations = newbuildConfigurations.Replace("\n\n", "\n");
/// Write new configurations into Unity XCode Project
var newconfigurationsFromUnityProject = configurationsFromUnityProject.Replace(buildConfigurations, newbuildConfigurations);
pbxprojText = pbxprojText.Replace(configurationsFromUnityProject, newconfigurationsFromUnityProject);
pbxprojText = pbxprojText.Replace(pbxProjectFullBuildConfiguration, newpbxProjectFullBuildConfiguration);
File.WriteAllText(pbxprojFile, pbxprojText);
}
I am also using an extension on Strings:
static class Helper {
public static string GetUntilOrEmpty(this string text, string stopAt = "-") {
if (!String.IsNullOrWhiteSpace(text)) {
int charLocation = text.IndexOf(stopAt, StringComparison.Ordinal);
if (charLocation > 0) {
return text.Substring(0, charLocation);
}
}
return String.Empty;
}
}
I tested this again and it seems like it's not really working :/ but I will continue working on this. I try to use xcode cli...
Info: There's no need for this.
I tested with Flutter 3.3.2 Beta and it's working like a charm :)
We are currently constrained to flutter 2.8.1 and required this to sync flavours. The solution suggested by @Ahmadre did not work in our scenario, so I slightly tweaked it and am posting here for others in a similar boat:
private static readonly string FlutterIOSPath = Path.GetFullPath(Path.Combine(ProjectPath, "../../ios"));
private static System.Random random = new System.Random();
private static string GenerateXCodeId()
{
const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
return new string(Enumerable.Repeat(chars, 24)
.Select(s => s[random.Next(s.Length)]).ToArray());
}
private static string[] GetFlutterRunnerFlavors()
{
/// Read existing flavors from iOS Flutter Project (except Runner Flavor itself)
string[] flutterFlavors = Directory.GetFiles($"{FlutterIOSPath}/Runner.xcodeproj/xcshareddata/xcschemes", "*.xcscheme").Where(s => !s.Contains("Runner.xcscheme")).ToArray();
/// If no flavors exists for this App skip flavor generation
if (flutterFlavors.Length == 0) return null;
string[] flavorNames = new string[flutterFlavors.Length];
for (int i = 0; i < flutterFlavors.Length; i++)
{
flavorNames[i] = Path.GetFileName(flutterFlavors[i]).Split(".")[0];
}
return flavorNames;
}
private static void AddXCodeFlavorToPBX(string targetPattern, string[] flavors, ref string pbxprojText)
{
/// Get Full Configuration for nth buildConfiguration
string pbxProjectFullBuildConfiguration = pbxprojText.Substring(pbxprojText.IndexOf("/* Begin XCBuildConfiguration section */")).GetUntilOrEmpty("/* End XCBuildConfiguration section */");
string newpbxProjectFullBuildConfiguration = pbxProjectFullBuildConfiguration;
/// Get buildConfigurations section from Unity-Project
var indexOfPBXProjectBuildConfigurationOfUnityIPhone = pbxprojText.IndexOf("/* Build configuration list for " + targetPattern + " */ = {");
var configurationsFromUnityProject = pbxprojText.Substring(indexOfPBXProjectBuildConfigurationOfUnityIPhone).GetUntilOrEmpty("};");
var buildConfigurations = configurationsFromUnityProject.Substring(configurationsFromUnityProject.IndexOf("buildConfigurations = (")).GetUntilOrEmpty(");").Replace("buildConfigurations = (", "");
string[] buildConfigurationItems = buildConfigurations.Substring(1).Split(",").Where(s => !s.Contains("ReleaseForProfiling") && !s.Contains("ReleaseForRunning")).ToArray();
var newbuildConfigurations = buildConfigurations;
foreach (var configuration in buildConfigurationItems)
{
if (configuration.Contains("/* ") && configuration.Contains(" */"))
{
var configName = configuration.GetUntilOrEmpty(" */").Split(" ").Last();
foreach (var fl in flavors)
{
var newXCodeId = GenerateXCodeId();
var oldId = configuration.Split(" ").First().Trim();
var newName = configuration.Replace($"{configName}", $"{configName}-{fl}").Replace(oldId, newXCodeId);
newbuildConfigurations += $"{newName},\n";
var index = pbxProjectFullBuildConfiguration.IndexOf(oldId + " " + $"/* {configName} */");
var sameConfig = pbxProjectFullBuildConfiguration
.Substring(index)
.GetUntilOrEmpty($"name = {configName};");
string newstr = newXCodeId + " " + $"/* {configName}-{fl} */ = " + "{";
var firstLine = sameConfig.Split(new[] { '\r', '\n' }).FirstOrDefault();
sameConfig = sameConfig.Replace(firstLine, newstr);
sameConfig += $"name = {configName}-{fl};\n" + "};\n";
newpbxProjectFullBuildConfiguration += sameConfig;
}
}
}
newbuildConfigurations = newbuildConfigurations.Replace("\n\n", "\n");
/// new configurations into Unity XCode Project
var newconfigurationsFromUnityProject = configurationsFromUnityProject.Replace(buildConfigurations, newbuildConfigurations);
pbxprojText = pbxprojText.Replace(configurationsFromUnityProject, newconfigurationsFromUnityProject);
pbxprojText = pbxprojText.Replace(pbxProjectFullBuildConfiguration, newpbxProjectFullBuildConfiguration);
}
private static void ModifyXCodeBuild()
{
var pbxprojFile = Path.Combine(IOSExportPath, "Unity-iPhone.xcodeproj/project.pbxproj");
var pbxprojText = File.ReadAllText(pbxprojFile);
var flutterFlavorNames = GetFlutterRunnerFlavors();
if (flutterFlavorNames == null) return;
AddXCodeFlavorToPBX("PBXProject \"Unity-iPhone\"", flutterFlavorNames, ref pbxprojText);
AddXCodeFlavorToPBX("PBXNativeTarget \"UnityFramework\"", flutterFlavorNames, ref pbxprojText);
AddXCodeFlavorToPBX("PBXNativeTarget \"Unity-iPhone\"", flutterFlavorNames, ref pbxprojText);
AddXCodeFlavorToPBX("PBXNativeTarget \"Unity-iPhone Tests\"", flutterFlavorNames, ref pbxprojText);
File.WriteAllText(pbxprojFile, pbxprojText);
}
We are currently constrained to flutter 2.8.1 and required this to sync flavours. The solution suggested by @Ahmadre did not work in our scenario, so I slightly tweaked it and am posting here for others in a similar boat:
private static readonly string FlutterIOSPath = Path.GetFullPath(Path.Combine(ProjectPath, "../../ios")); private static System.Random random = new System.Random(); private static string GenerateXCodeId() { const string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; return new string(Enumerable.Repeat(chars, 24) .Select(s => s[random.Next(s.Length)]).ToArray()); } private static string[] GetFlutterRunnerFlavors() { /// Read existing flavors from iOS Flutter Project (except Runner Flavor itself) string[] flutterFlavors = Directory.GetFiles($"{FlutterIOSPath}/Runner.xcodeproj/xcshareddata/xcschemes", "*.xcscheme").Where(s => !s.Contains("Runner.xcscheme")).ToArray(); /// If no flavors exists for this App skip flavor generation if (flutterFlavors.Length == 0) return null; string[] flavorNames = new string[flutterFlavors.Length]; for (int i = 0; i < flutterFlavors.Length; i++) { flavorNames[i] = Path.GetFileName(flutterFlavors[i]).Split(".")[0]; } return flavorNames; } private static void AddXCodeFlavorToPBX(string targetPattern, string[] flavors, ref string pbxprojText) { /// Get Full Configuration for nth buildConfiguration string pbxProjectFullBuildConfiguration = pbxprojText.Substring(pbxprojText.IndexOf("/* Begin XCBuildConfiguration section */")).GetUntilOrEmpty("/* End XCBuildConfiguration section */"); string newpbxProjectFullBuildConfiguration = pbxProjectFullBuildConfiguration; /// Get buildConfigurations section from Unity-Project var indexOfPBXProjectBuildConfigurationOfUnityIPhone = pbxprojText.IndexOf("/* Build configuration list for " + targetPattern + " */ = {"); var configurationsFromUnityProject = pbxprojText.Substring(indexOfPBXProjectBuildConfigurationOfUnityIPhone).GetUntilOrEmpty("};"); var buildConfigurations = configurationsFromUnityProject.Substring(configurationsFromUnityProject.IndexOf("buildConfigurations = (")).GetUntilOrEmpty(");").Replace("buildConfigurations = (", ""); string[] buildConfigurationItems = buildConfigurations.Substring(1).Split(",").Where(s => !s.Contains("ReleaseForProfiling") && !s.Contains("ReleaseForRunning")).ToArray(); var newbuildConfigurations = buildConfigurations; foreach (var configuration in buildConfigurationItems) { if (configuration.Contains("/* ") && configuration.Contains(" */")) { var configName = configuration.GetUntilOrEmpty(" */").Split(" ").Last(); foreach (var fl in flavors) { var newXCodeId = GenerateXCodeId(); var oldId = configuration.Split(" ").First().Trim(); var newName = configuration.Replace($"{configName}", $"{configName}-{fl}").Replace(oldId, newXCodeId); newbuildConfigurations += $"{newName},\n"; var index = pbxProjectFullBuildConfiguration.IndexOf(oldId + " " + $"/* {configName} */"); var sameConfig = pbxProjectFullBuildConfiguration .Substring(index) .GetUntilOrEmpty($"name = {configName};"); string newstr = newXCodeId + " " + $"/* {configName}-{fl} */ = " + "{"; var firstLine = sameConfig.Split(new[] { '\r', '\n' }).FirstOrDefault(); sameConfig = sameConfig.Replace(firstLine, newstr); sameConfig += $"name = {configName}-{fl};\n" + "};\n"; newpbxProjectFullBuildConfiguration += sameConfig; } } } newbuildConfigurations = newbuildConfigurations.Replace("\n\n", "\n"); /// new configurations into Unity XCode Project var newconfigurationsFromUnityProject = configurationsFromUnityProject.Replace(buildConfigurations, newbuildConfigurations); pbxprojText = pbxprojText.Replace(configurationsFromUnityProject, newconfigurationsFromUnityProject); pbxprojText = pbxprojText.Replace(pbxProjectFullBuildConfiguration, newpbxProjectFullBuildConfiguration); } private static void ModifyXCodeBuild() { var pbxprojFile = Path.Combine(IOSExportPath, "Unity-iPhone.xcodeproj/project.pbxproj"); var pbxprojText = File.ReadAllText(pbxprojFile); var flutterFlavorNames = GetFlutterRunnerFlavors(); if (flutterFlavorNames == null) return; AddXCodeFlavorToPBX("PBXProject \"Unity-iPhone\"", flutterFlavorNames, ref pbxprojText); AddXCodeFlavorToPBX("PBXNativeTarget \"UnityFramework\"", flutterFlavorNames, ref pbxprojText); AddXCodeFlavorToPBX("PBXNativeTarget \"Unity-iPhone\"", flutterFlavorNames, ref pbxprojText); AddXCodeFlavorToPBX("PBXNativeTarget \"Unity-iPhone Tests\"", flutterFlavorNames, ref pbxprojText); File.WriteAllText(pbxprojFile, pbxprojText); }
Thank you so much for your solution :). Yes it needed the generated Ids but I wouldn't stick to my solution.
We have a XCodePostBuild File which accesses directly XCode (see: https://docs.unity3d.com/ScriptReference/iOS.Xcode.PBXProject.AddBuildConfig.html)
I try to solve it with that.
Here's the solution with XcodePostBuild.cs:
/// <summary>
/// Retrieves all BuildConfigurations from Flutter Project (Flavors)
/// </summary>
private static List<string> GetFlutterRunnerFlavors() {
/// Read existing flavors from iOS Flutter Project (except Runner Flavor itself)
string[] flutterFlavors = Directory.GetFiles($"{FlutterIOSPath}/Runner.xcodeproj/xcshareddata/xcschemes", "*.xcscheme");
/// If no flavors exists for this App skip flavor generation
if (flutterFlavors.Length == 0) return null;
List<string> flavorNames = new List<string> { };
for (int i = 0; i < flutterFlavors.Length; i++) {
var name = Path.GetFileName(flutterFlavors[i]).Split(".")[0];
if (name.Contains("Runner")) continue;
flavorNames.Add(name);
}
return flavorNames;
}
/// <summary>
/// We need to reflect the BuildConfiguration into the UnityFramework target.
/// This means for example Release-[flavor], Profile-[flavor], Debug-[flavor] or any other flavored configurations from Flutter.
/// Those are needed for seemless builds from Xcode.
/// </summary>
/// <param name="pathToBuildProject"></param>
private static void ReflectBuildConfigurations(string pathToBuildProject)
{
List<string> flutterFlavorNames = GetFlutterRunnerFlavors();
if (flutterFlavorNames == null) return;
var buildTargetTypes = new List<string> { "Debug", "Release", "Profile" };
var unityPBX = new PBXProject();
var unityPBXPath = Path.Combine(pathToBuildProject, "Unity-iPhone.xcodeproj/project.pbxproj");
unityPBX.ReadFromFile(unityPBXPath);
for (int t = 0; t < buildTargetTypes.Count; t++) {
for (int i = 0; i < flutterFlavorNames.Count; i++) {
unityPBX.AddBuildConfig($"{buildTargetTypes[t]}-{flutterFlavorNames[i]}");
}
}
// Persist changes
unityPBX.WriteToFile(unityPBXPath);
}