flutter-unity-view-widget icon indicating copy to clipboard operation
flutter-unity-view-widget copied to clipboard

[iOS | flavors]: Fix sync flavors for Unity Project

Open Ahmadre opened this issue 3 years ago • 1 comments

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;
	}
}

Ahmadre avatar Jun 16 '22 13:06 Ahmadre

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...

Ahmadre avatar Jun 16 '22 15:06 Ahmadre

Info: There's no need for this.

I tested with Flutter 3.3.2 Beta and it's working like a charm :)

Ahmadre avatar Sep 29 '22 13:09 Ahmadre

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);
  }

Jinl21 avatar Oct 12 '22 16:10 Jinl21

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.

Ahmadre avatar Oct 09 '23 07:10 Ahmadre

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);
    }

Ahmadre avatar Oct 09 '23 09:10 Ahmadre