From 9094725e0d0b5a573e100c2277cda74798bf158f Mon Sep 17 00:00:00 2001 From: a-maurice Date: Wed, 3 Sep 2025 17:23:38 -0700 Subject: [PATCH 1/5] Add Swift Package Manager support --- source/IOSResolver/IOSResolver.csproj | 1 + source/IOSResolver/src/IOSResolver.cs | 55 +++- .../src/IOSResolverSettingsDialog.cs | 15 + source/IOSResolver/src/SwiftPackageManager.cs | 278 ++++++++++++++++++ 4 files changed, 345 insertions(+), 4 deletions(-) create mode 100644 source/IOSResolver/src/SwiftPackageManager.cs diff --git a/source/IOSResolver/IOSResolver.csproj b/source/IOSResolver/IOSResolver.csproj index e16e2abf..7acfcc0f 100644 --- a/source/IOSResolver/IOSResolver.csproj +++ b/source/IOSResolver/IOSResolver.csproj @@ -68,6 +68,7 @@ + diff --git a/source/IOSResolver/src/IOSResolver.cs b/source/IOSResolver/src/IOSResolver.cs index a272810e..70359fe5 100644 --- a/source/IOSResolver/src/IOSResolver.cs +++ b/source/IOSResolver/src/IOSResolver.cs @@ -405,6 +405,9 @@ protected override bool Read(string filename, Logger logger) { private static SortedDictionary pods = new SortedDictionary(); + // List of pods to ignore because they are replaced by Swift packages. + private static HashSet podsToIgnore = new HashSet(); + // Order of post processing operations. private const int BUILD_ORDER_REFRESH_DEPENDENCIES = 10; private const int BUILD_ORDER_CHECK_COCOAPODS_INSTALL = 20; @@ -510,6 +513,10 @@ protected override bool Read(string filename, Logger logger) { private const string PREFERENCE_PODFILE_ALLOW_PODS_IN_MULTIPLE_TARGETS = PREFERENCE_NAMESPACE + "PodfileAllowPodsInMultipleTargets"; + // Whether to use Swift Package Manager for dependency resolution. + private const string PREFERENCE_SWIFT_PACKAGE_MANAGER_ENABLED = + PREFERENCE_NAMESPACE + "SwiftPackageManagerEnabled"; + // List of preference keys, used to restore default settings. private static string[] PREFERENCE_KEYS = new [] { PREFERENCE_COCOAPODS_INSTALL_ENABLED, @@ -525,7 +532,8 @@ protected override bool Read(string filename, Logger logger) { PREFERENCE_SWIFT_FRAMEWORK_SUPPORT_WORKAROUND, PREFERENCE_SWIFT_LANGUAGE_VERSION, PREFERENCE_PODFILE_ALWAYS_ADD_MAIN_TARGET, - PREFERENCE_PODFILE_ALLOW_PODS_IN_MULTIPLE_TARGETS + PREFERENCE_PODFILE_ALLOW_PODS_IN_MULTIPLE_TARGETS, + PREFERENCE_SWIFT_PACKAGE_MANAGER_ENABLED }; // Whether the xcode extension was successfully loaded. @@ -605,6 +613,9 @@ protected override bool Read(string filename, Logger logger) { // Parses dependencies from XML dependency files. private static IOSXmlDependencies xmlDependencies = new IOSXmlDependencies(); + // Parses SPM dependencies from XML dependency files. + private static SwiftPackageManager spmDependencies = new SwiftPackageManager(); + // Project level settings for this module. private static ProjectSettings settings = new ProjectSettings(PREFERENCE_NAMESPACE); @@ -1158,6 +1169,19 @@ public static bool PodfileAllowPodsInMultipleTargets { } } + /// + /// Whether to use Swift Package Manager for dependency resolution. + /// If enabled, the resolver will attempt to use SPM for packages that define SPM support, + /// falling back to Cocoapods if disabled or if the package does not support SPM. + /// + public static bool SwiftPackageManagerEnabled { + get { return settings.GetBool(PREFERENCE_SWIFT_PACKAGE_MANAGER_ENABLED, + defaultValue: true); } + set { + settings.SetBool(PREFERENCE_SWIFT_PACKAGE_MANAGER_ENABLED, value); + } + } + /// /// Whether to use project level settings. @@ -2148,6 +2172,17 @@ internal static void AddDummySwiftFile( File.WriteAllText(pbxprojPath, project.WriteToString()); } + [PostProcessBuildAttribute(35)] + public static void OnPostProcessResolveSwiftPackages(BuildTarget buildTarget, + string pathToBuiltProject) { + if (!SwiftPackageManagerEnabled) { + return; + } + var resolvedPackages = SwiftPackageManager.Resolve(spmDependencies.SwiftPackages, logger); + SwiftPackageManager.AddPackagesToProject(resolvedPackages, pathToBuiltProject, logger); + podsToIgnore = SwiftPackageManager.GetReplacedPods(resolvedPackages); + } + /// /// Post-processing build step to generate the podfile for ios. /// @@ -2155,7 +2190,7 @@ internal static void AddDummySwiftFile( public static void OnPostProcessGenPodfile(BuildTarget buildTarget, string pathToBuiltProject) { if (!InjectDependencies() || !PodfileGenerationEnabled) return; - GenPodfile(buildTarget, pathToBuiltProject); + GenPodfile(buildTarget, pathToBuiltProject, podsToIgnore); } /// @@ -2299,7 +2334,8 @@ private static string GeneratePodfileSourcesSection() { // Mono runtime from loading the Xcode API before calling the post // processing step. public static void GenPodfile(BuildTarget buildTarget, - string pathToBuiltProject) { + string pathToBuiltProject, + HashSet podsToIgnore = null) { analytics.Report("generatepodfile", "Generate Podfile"); string podfilePath = GetPodfilePath(pathToBuiltProject); @@ -2341,6 +2377,10 @@ public static void GenPodfile(BuildTarget buildTarget, foreach (var target in XcodeTargetNames) { file.WriteLine(String.Format("target '{0}' do", target)); foreach(var pod in pods.Values) { + if (podsToIgnore != null && podsToIgnore.Contains(pod.name)) { + Log(String.Format("Skipping pod {0} because it is replaced by a Swift Package.", pod.name), verbose: true); + continue; + } file.WriteLine(String.Format(" {0}", pod.PodFilePodLine)); } file.WriteLine("end"); @@ -2351,6 +2391,9 @@ public static void GenPodfile(BuildTarget buildTarget, bool allowPodsInMultipleTargets = PodfileAllowPodsInMultipleTargets; int podAdded = 0; foreach(var pod in pods.Values) { + if (podsToIgnore != null && podsToIgnore.Contains(pod.name)) { + continue; + } if (pod.addToAllTargets) { file.WriteLine(String.Format(" {0}{1}", allowPodsInMultipleTargets ? "" : "# ", @@ -2908,10 +2951,14 @@ private static void RefreshXmlDependencies() { foreach (var podName in podsToRemove) { pods.Remove(podName); } + spmDependencies.SwiftPackages.Clear(); + podsToIgnore.Clear(); + // Clear all sources (only can be set via XML config). Pod.Sources = new List>(); - // Read pod specifications from XML dependencies. + // Read pod and spm specifications from XML dependencies. xmlDependencies.ReadAll(logger); + spmDependencies.ReadAll(logger); } } diff --git a/source/IOSResolver/src/IOSResolverSettingsDialog.cs b/source/IOSResolver/src/IOSResolverSettingsDialog.cs index e8f7001d..c0f4412b 100644 --- a/source/IOSResolver/src/IOSResolverSettingsDialog.cs +++ b/source/IOSResolver/src/IOSResolverSettingsDialog.cs @@ -43,6 +43,7 @@ private class Settings { internal string swiftLanguageVersion; internal bool podfileAlwaysAddMainTarget; internal bool podfileAllowPodsInMultipleTargets; + internal bool swiftPackageManagerEnabled; internal bool useProjectSettings; internal EditorMeasurement.Settings analyticsSettings; @@ -64,6 +65,7 @@ internal Settings() { swiftLanguageVersion = IOSResolver.SwiftLanguageVersion; podfileAlwaysAddMainTarget = IOSResolver.PodfileAlwaysAddMainTarget; podfileAllowPodsInMultipleTargets = IOSResolver.PodfileAllowPodsInMultipleTargets; + swiftPackageManagerEnabled = IOSResolver.SwiftPackageManagerEnabled; useProjectSettings = IOSResolver.UseProjectSettings; analyticsSettings = new EditorMeasurement.Settings(IOSResolver.analytics); } @@ -86,6 +88,7 @@ internal void Save() { IOSResolver.SwiftLanguageVersion = swiftLanguageVersion; IOSResolver.PodfileAlwaysAddMainTarget = podfileAlwaysAddMainTarget; IOSResolver.PodfileAllowPodsInMultipleTargets = podfileAllowPodsInMultipleTargets; + IOSResolver.SwiftPackageManagerEnabled = swiftPackageManagerEnabled; IOSResolver.UseProjectSettings = useProjectSettings; analyticsSettings.Save(); } @@ -149,6 +152,18 @@ public void OnGUI() { scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); + GUILayout.BeginHorizontal(); + GUILayout.Label("Swift Package Manager Integration", EditorStyles.boldLabel); + settings.swiftPackageManagerEnabled = + EditorGUILayout.Toggle(settings.swiftPackageManagerEnabled); + GUILayout.EndHorizontal(); + GUILayout.Label("Use Swift Package Manager to resolve dependencies that support it. " + + "If this is enabled, the resolver will prioritize SPM packages and " + + "fall back to Cocoapods for any dependencies that do not have an " + + "SPM equivalent specified."); + + GUILayout.Box("", GUILayout.ExpandWidth(true), GUILayout.Height(1)); + GUILayout.BeginHorizontal(); GUILayout.Label("Podfile Generation", EditorStyles.boldLabel); settings.podfileGenerationEnabled = diff --git a/source/IOSResolver/src/SwiftPackageManager.cs b/source/IOSResolver/src/SwiftPackageManager.cs new file mode 100644 index 00000000..da5cf528 --- /dev/null +++ b/source/IOSResolver/src/SwiftPackageManager.cs @@ -0,0 +1,278 @@ +// +// Copyright (C) 2022 Google Inc. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// +#if UNITY_IOS + +using Google.JarResolver; +using GooglePlayServices; +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Xml.Linq; +using UnityEditor; +using UnityEditor.iOS.Xcode; + +namespace Google { + /// + /// Represents a single Swift package framework to be added to the project. + /// This corresponds to the tag. + /// + internal class SwiftPackage { + /// + /// Name of the package framework. (e.g. "FirebaseAnalytics") + /// + public string Name { get; set; } + + /// + /// Whether the framework should be weakly linked. Defaults to false. + /// + public bool Weak { get; set; } + + /// + /// Comma-separated list of Cocoapods that this package replaces. + /// (e.g. "Firebase/Analytics,Firebase/Core") + /// + public string ReplacesPod { get; set; } + + /// + /// A reference back to the remote package this framework belongs to. + /// + public RemoteSwiftPackage RemotePackage { get; set; } + } + + /// + /// Represents a remote Swift package repository. + /// This corresponds to the tag. + /// + internal class RemoteSwiftPackage { + /// + /// The git URL of the package repository. + /// + public string Url { get; set; } + + /// + /// The version string for the package. (e.g. "9.4.0") + /// + public string Version { get; set; } + + /// + /// Whether to use "upToNextMinor" versioning. + /// + public bool UpToNextMinor { get; set; } + + /// + /// Whether to use "upToNextMajor" versioning. + /// + public bool UpToNextMajor { get; set; } + + /// + /// List of the specific package frameworks defined within this remote package. + /// + public List Packages { get; set; } = new List(); + + /// + /// The file path where this package was defined. + /// + public string DefinedIn { get; set; } + } + + /// + /// Parses Swift Package Manager dependencies from *Dependencies.xml files. + /// + internal class SwiftPackageManager : XmlDependencies { + + /// + /// List of packages that have been parsed. + /// + public List SwiftPackages = new List(); + + public SwiftPackageManager() { + dependencyType = "SPM dependencies"; + } + + /// + /// Reads and parses the dependencies from a given XML file. + /// + protected override bool Read(string filename, Logger logger) { + var packages = new List(); + var trueStrings = new HashSet { "true", "1" }; + + try { + XDocument doc = XDocument.Load(filename); + foreach (var remotePackageElement in doc.Descendants("remoteSwiftPackage")) { + var remotePackage = new RemoteSwiftPackage { + Url = (string)remotePackageElement.Attribute("url"), + Version = (string)remotePackageElement.Attribute("version"), + UpToNextMinor = trueStrings.Contains(((string)remotePackageElement.Attribute("upToNextMinor") ?? "").ToLower()), + UpToNextMajor = trueStrings.Contains(((string)remotePackageElement.Attribute("upToNextMajor") ?? "").ToLower()), + DefinedIn = filename + }; + + if (string.IsNullOrEmpty(remotePackage.Url) || string.IsNullOrEmpty(remotePackage.Version)) { + logger.Log(string.Format("Skipping remoteSwiftPackage in {0} due to missing 'url' or 'version' attribute.", filename), level: LogLevel.Warning); + continue; + } + + foreach (var packageElement in remotePackageElement.Elements("swiftPackage")) { + var swiftPackage = new SwiftPackage { + Name = (string)packageElement.Attribute("name"), + Weak = trueStrings.Contains(((string)packageElement.Attribute("weak") ?? "").ToLower()), + ReplacesPod = (string)packageElement.Attribute("replacesPod"), + RemotePackage = remotePackage + }; + + if (string.IsNullOrEmpty(swiftPackage.Name)) { + logger.Log(string.Format("Skipping swiftPackage in {0} due to missing 'name' attribute.", filename), level: LogLevel.Warning); + continue; + } + remotePackage.Packages.Add(swiftPackage); + } + packages.Add(remotePackage); + } + SwiftPackages.AddRange(packages); + } catch (System.Exception e) { + logger.Log(string.Format("Error parsing Swift Package Manager dependencies from {0}: {1}", filename, e.ToString()), level: LogLevel.Error); + return false; + } + return true; + } + + /// + /// Resolves the Swift Package Manager dependencies, handling conflicts. + /// + /// The list of swift packages to resolve. + /// A logger for reporting messages. + /// A list of resolved packages. + internal static List Resolve(List packages, Logger logger) { + var resolvedPackages = new Dictionary(); + + // Resolve remote package version conflicts. Highest version wins. + foreach (var package in packages) { + if (resolvedPackages.TryGetValue(package.Url, out var existingPackage)) { + var existingVersion = new Version(existingPackage.Version); + var newVersion = new Version(package.Version); + + if (newVersion > existingVersion) { + logger.Log(string.Format( + "SPM package version conflict for {0}. Using version {1} from {2} instead of {3} from {4}.", + package.Url, package.Version, package.DefinedIn, existingPackage.Version, existingPackage.DefinedIn), + level: LogLevel.Warning); + package.Packages.AddRange(existingPackage.Packages); + resolvedPackages[package.Url] = package; + } else { + existingPackage.Packages.AddRange(package.Packages); + } + } else { + resolvedPackages[package.Url] = package; + } + } + + // Resolve inner swiftPackage conflicts. + foreach (var remotePackage in resolvedPackages.Values) { + var packageGroups = remotePackage.Packages.GroupBy(p => p.Name) + .ToDictionary(g => g.Key, g => g.ToList()); + + var finalPackages = new List(); + foreach (var group in packageGroups) { + if (group.Value.Count == 1) { + finalPackages.Add(group.Value.First()); + continue; + } + + var mergedPackage = new SwiftPackage { + Name = group.Key, + Weak = group.Value.All(p => p.Weak), + ReplacesPod = string.Join(",", group.Value.Select(p => p.ReplacesPod) + .Where(rp => !string.IsNullOrEmpty(rp)) + .SelectMany(rp => rp.Split(',')) + .Select(p => p.Trim()) + .Distinct()), + RemotePackage = remotePackage + }; + finalPackages.Add(mergedPackage); + } + remotePackage.Packages = finalPackages; + } + + return resolvedPackages.Values.ToList(); + } + + /// + /// Extracts the list of Cocoapods that are replaced by the given Swift packages. + /// + /// A list of resolved Swift packages. + /// A unique list of pod names to be removed. + internal static HashSet GetReplacedPods(List resolvedPackages) { + var replacedPods = new HashSet(); + foreach (var package in resolvedPackages) { + foreach (var swiftPackage in package.Packages) { + if (!string.IsNullOrEmpty(swiftPackage.ReplacesPod)) { + foreach (var pod in swiftPackage.ReplacesPod.Split(',')) { + replacedPods.Add(pod.Trim()); + } + } + } + } + return replacedPods; + } + + /// + /// Modifies the Xcode project to add the Swift Package dependencies. + /// + /// The list of resolved packages to add. + /// The path to the Xcode project. + /// A logger for reporting messages. + internal static void AddPackagesToProject(List resolvedPackages, string projectPath, Logger logger) { + if (VersionHandler.GetUnityVersionMajorMinor() < 2021.3f) { + logger.Log("Swift Package Manager integration is only supported in Unity 2021.3 and newer. Disabling.", level: LogLevel.Warning); + return; + } + + string pbxProjectPath = PBXProject.GetPBXProjectPath(projectPath); + PBXProject project = new PBXProject(); + project.ReadFromFile(pbxProjectPath); + + string mainTargetGuid = project.GetUnityMainTargetGuid(); + + foreach (var remotePackage in resolvedPackages) { + try { + string methodName; + if (remotePackage.UpToNextMajor) { + methodName = "AddRemotePackageReferenceAtVersionUpToNextMajor"; + } else if (remotePackage.UpToNextMinor) { + methodName = "AddRemotePackageReferenceAtVersionUpToNextMinor"; + } else { + methodName = "AddRemotePackageReferenceAtVersion"; + } + + VersionHandler.InvokeInstanceMethod(project, methodName, new object[] { mainTargetGuid, remotePackage.Url, remotePackage.Version }); + logger.Log(string.Format("Added SPM package {0} version {1} to project.", remotePackage.Url, remotePackage.Version), level: LogLevel.Info); + + foreach (var swiftPackage in remotePackage.Packages) { + VersionHandler.InvokeInstanceMethod(project, "AddRemotePackageFrameworkToProject", new object[] { mainTargetGuid, swiftPackage.Name, swiftPackage.Weak }); + logger.Log(string.Format(" - Added framework {0} to project.", swiftPackage.Name), level: LogLevel.Info); + } + } catch (Exception e) { + logger.Log(string.Format("Failed to add Swift Package {0}. Error: {1}", remotePackage.Url, e.Message), level: LogLevel.Error); + } + } + + project.WriteToFile(pbxProjectPath); + } + } +} +#endif // UNITY_IOS \ No newline at end of file From 274dec141c81d9a8e7cb7cb396ee7c97bcd6994a Mon Sep 17 00:00:00 2001 From: a-maurice Date: Thu, 4 Sep 2025 14:17:57 -0700 Subject: [PATCH 2/5] Don't generate the Podfile when empty --- source/IOSResolver/src/IOSResolver.cs | 41 +++++++++++++------ source/IOSResolver/src/SwiftPackageManager.cs | 13 +++--- 2 files changed, 35 insertions(+), 19 deletions(-) diff --git a/source/IOSResolver/src/IOSResolver.cs b/source/IOSResolver/src/IOSResolver.cs index 70359fe5..41f2ffe6 100644 --- a/source/IOSResolver/src/IOSResolver.cs +++ b/source/IOSResolver/src/IOSResolver.cs @@ -407,6 +407,9 @@ protected override bool Read(string filename, Logger logger) { // List of pods to ignore because they are replaced by Swift packages. private static HashSet podsToIgnore = new HashSet(); + // Flag used to denoted if generating the Podfile was skipped because of no pods. + // Used by subsequent steps to know they should be skipped as well. + private static bool podfileGenerationSkipped = false; // Order of post processing operations. private const int BUILD_ORDER_REFRESH_DEPENDENCIES = 10; @@ -2352,6 +2355,21 @@ public static void GenPodfile(BuildTarget buildTarget, } } + var filteredPods = new List(); + foreach (var pod in pods.Values) { + if (podsToIgnore != null && podsToIgnore.Contains(pod.name)) { + Log(String.Format("Skipping pod {0} because it is replaced by a Swift Package.", pod.name), verbose: true); + continue; + } + filteredPods.Add(pod); + } + if (filteredPods.Count == 0) { + Log("Found no pods to add, skipping generation of the Podfile"); + podfileGenerationSkipped = true; + return; + } + podfileGenerationSkipped = false; + Log(String.Format("Generating Podfile {0} with {1} integration.", podfilePath, (CocoapodsWorkspaceIntegrationEnabled ? "Xcode workspace" : (CocoapodsProjectIntegrationEnabled ? "Xcode project" : "no target"))), @@ -2376,11 +2394,7 @@ public static void GenPodfile(BuildTarget buildTarget, foreach (var target in XcodeTargetNames) { file.WriteLine(String.Format("target '{0}' do", target)); - foreach(var pod in pods.Values) { - if (podsToIgnore != null && podsToIgnore.Contains(pod.name)) { - Log(String.Format("Skipping pod {0} because it is replaced by a Swift Package.", pod.name), verbose: true); - continue; - } + foreach(var pod in filteredPods) { file.WriteLine(String.Format(" {0}", pod.PodFilePodLine)); } file.WriteLine("end"); @@ -2390,10 +2404,7 @@ public static void GenPodfile(BuildTarget buildTarget, file.WriteLine(String.Format("target '{0}' do", XcodeMainTargetName)); bool allowPodsInMultipleTargets = PodfileAllowPodsInMultipleTargets; int podAdded = 0; - foreach(var pod in pods.Values) { - if (podsToIgnore != null && podsToIgnore.Contains(pod.name)) { - continue; - } + foreach(var pod in filteredPods) { if (pod.addToAllTargets) { file.WriteLine(String.Format(" {0}{1}", allowPodsInMultipleTargets ? "" : "# ", @@ -2420,7 +2431,7 @@ public static void GenPodfile(BuildTarget buildTarget, int maxProperties = 0; int maxSources = Pod.Sources.Count; int fromXmlFileCount = 0; - foreach (var pod in pods.Values) { + foreach (var pod in filteredPods) { maxProperties = Math.Max(maxProperties, pod.propertiesByName.Count); maxSources = Math.Max(maxSources, pod.sources.Count); if (!String.IsNullOrEmpty(pod.version)) versionCount++; @@ -2430,7 +2441,7 @@ public static void GenPodfile(BuildTarget buildTarget, } analytics.Report("generatepodfile/podinfo", new KeyValuePair[] { - new KeyValuePair("numPods", pods.Count.ToString()), + new KeyValuePair("numPods", filteredPods.Count.ToString()), new KeyValuePair("numPodsWithVersions", versionCount.ToString()), new KeyValuePair("numLocalPods", @@ -2779,6 +2790,11 @@ public static void OnPostProcessInstallPods(BuildTarget buildTarget, string pathToBuiltProject) { if (!InjectDependencies() || !PodfileGenerationEnabled) return; + // If the Podfile Generation was skipped because of no pods to install, skip this step. + if (podfileGenerationSkipped) { + return; + } + if(EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS) { UpdateTargetIosSdkVersion(true); } @@ -2885,7 +2901,8 @@ public static void OnPostProcessUpdateProjectDeps( BuildTarget buildTarget, string pathToBuiltProject) { if (!InjectDependencies() || !PodfileGenerationEnabled || !CocoapodsProjectIntegrationEnabled || // Early out for Workspace level integration. - !cocoapodsToolsInstallPresent) { + !cocoapodsToolsInstallPresent || + podfileGenerationSkipped) { return; } diff --git a/source/IOSResolver/src/SwiftPackageManager.cs b/source/IOSResolver/src/SwiftPackageManager.cs index da5cf528..d76a3583 100644 --- a/source/IOSResolver/src/SwiftPackageManager.cs +++ b/source/IOSResolver/src/SwiftPackageManager.cs @@ -24,7 +24,6 @@ using System.Reflection; using System.Xml.Linq; using UnityEditor; -using UnityEditor.iOS.Xcode; namespace Google { /// @@ -242,11 +241,11 @@ internal static void AddPackagesToProject(List resolvedPacka return; } - string pbxProjectPath = PBXProject.GetPBXProjectPath(projectPath); - PBXProject project = new PBXProject(); + string pbxProjectPath = UnityEditor.iOS.Xcode.PBXProject.GetPBXProjectPath(projectPath); + var project = new UnityEditor.iOS.Xcode.PBXProject(); project.ReadFromFile(pbxProjectPath); - string mainTargetGuid = project.GetUnityMainTargetGuid(); + string frameworkTargetGuid = project.GetUnityFrameworkTargetGuid(); foreach (var remotePackage in resolvedPackages) { try { @@ -259,11 +258,11 @@ internal static void AddPackagesToProject(List resolvedPacka methodName = "AddRemotePackageReferenceAtVersion"; } - VersionHandler.InvokeInstanceMethod(project, methodName, new object[] { mainTargetGuid, remotePackage.Url, remotePackage.Version }); + var packageGuid = VersionHandler.InvokeInstanceMethod(project, methodName, new object[] { remotePackage.Url, remotePackage.Version }); logger.Log(string.Format("Added SPM package {0} version {1} to project.", remotePackage.Url, remotePackage.Version), level: LogLevel.Info); foreach (var swiftPackage in remotePackage.Packages) { - VersionHandler.InvokeInstanceMethod(project, "AddRemotePackageFrameworkToProject", new object[] { mainTargetGuid, swiftPackage.Name, swiftPackage.Weak }); + VersionHandler.InvokeInstanceMethod(project, "AddRemotePackageFrameworkToProject", new object[] { frameworkTargetGuid, swiftPackage.Name, packageGuid, swiftPackage.Weak }); logger.Log(string.Format(" - Added framework {0} to project.", swiftPackage.Name), level: LogLevel.Info); } } catch (Exception e) { @@ -275,4 +274,4 @@ internal static void AddPackagesToProject(List resolvedPacka } } } -#endif // UNITY_IOS \ No newline at end of file +#endif // UNITY_IOS From f2523c0109653aa802390e75dd5d21194f8acd27 Mon Sep 17 00:00:00 2001 From: a-maurice Date: Thu, 4 Sep 2025 14:38:31 -0700 Subject: [PATCH 3/5] Fix up for gradle build --- source/IOSResolver/IOSResolver.csproj | 1 + source/IOSResolver/src/SwiftPackageManager.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/source/IOSResolver/IOSResolver.csproj b/source/IOSResolver/IOSResolver.csproj index 7acfcc0f..7b0a40d3 100644 --- a/source/IOSResolver/IOSResolver.csproj +++ b/source/IOSResolver/IOSResolver.csproj @@ -50,6 +50,7 @@ + ..\AndroidResolver\bin\Release\Google.JarResolver.dll diff --git a/source/IOSResolver/src/SwiftPackageManager.cs b/source/IOSResolver/src/SwiftPackageManager.cs index d76a3583..6665feb5 100644 --- a/source/IOSResolver/src/SwiftPackageManager.cs +++ b/source/IOSResolver/src/SwiftPackageManager.cs @@ -199,7 +199,7 @@ internal static List Resolve(List packag .Where(rp => !string.IsNullOrEmpty(rp)) .SelectMany(rp => rp.Split(',')) .Select(p => p.Trim()) - .Distinct()), + .Distinct().ToArray()), RemotePackage = remotePackage }; finalPackages.Add(mergedPackage); From 9e9e3119a08a27dbcdd74eff3e333b253a1a5121 Mon Sep 17 00:00:00 2001 From: a-maurice Date: Tue, 9 Sep 2025 14:16:47 -0700 Subject: [PATCH 4/5] Update IOSResolver.cs --- source/IOSResolver/src/IOSResolver.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/IOSResolver/src/IOSResolver.cs b/source/IOSResolver/src/IOSResolver.cs index 41f2ffe6..d1aa44af 100644 --- a/source/IOSResolver/src/IOSResolver.cs +++ b/source/IOSResolver/src/IOSResolver.cs @@ -1315,7 +1315,7 @@ public static bool PodPresent(string pod) { private static bool InjectDependencies() { return (EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS || EditorUserBuildSettings.activeBuildTarget == BuildTarget.tvOS) && - Enabled && pods.Count > 0; + Enabled && (pods.Count > 0 || spmDependencies.SwiftPackages.Count > 0); } /// @@ -2178,7 +2178,7 @@ internal static void AddDummySwiftFile( [PostProcessBuildAttribute(35)] public static void OnPostProcessResolveSwiftPackages(BuildTarget buildTarget, string pathToBuiltProject) { - if (!SwiftPackageManagerEnabled) { + if (!InjectDependencies() || !SwiftPackageManagerEnabled) { return; } var resolvedPackages = SwiftPackageManager.Resolve(spmDependencies.SwiftPackages, logger); From fa84de79d936ee6cdc978b4427ea1b384f83e27b Mon Sep 17 00:00:00 2001 From: a-maurice Date: Tue, 16 Sep 2025 17:46:09 -0700 Subject: [PATCH 5/5] Add logic to allow empty Podfiles --- source/IOSResolver/src/IOSResolver.cs | 22 +++++++++++++++++-- .../src/IOSResolverSettingsDialog.cs | 14 ++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/source/IOSResolver/src/IOSResolver.cs b/source/IOSResolver/src/IOSResolver.cs index d1aa44af..5345bb93 100644 --- a/source/IOSResolver/src/IOSResolver.cs +++ b/source/IOSResolver/src/IOSResolver.cs @@ -516,6 +516,10 @@ protected override bool Read(string filename, Logger logger) { private const string PREFERENCE_PODFILE_ALLOW_PODS_IN_MULTIPLE_TARGETS = PREFERENCE_NAMESPACE + "PodfileAllowPodsInMultipleTargets"; + // Whether to allow empty Podfile generation. + private const string PREFERENCE_ALLOW_EMPTY_PODFILE_GENERATION = + PREFERENCE_NAMESPACE + "AllowEmptyPodfileGeneration"; + // Whether to use Swift Package Manager for dependency resolution. private const string PREFERENCE_SWIFT_PACKAGE_MANAGER_ENABLED = PREFERENCE_NAMESPACE + "SwiftPackageManagerEnabled"; @@ -536,6 +540,7 @@ protected override bool Read(string filename, Logger logger) { PREFERENCE_SWIFT_LANGUAGE_VERSION, PREFERENCE_PODFILE_ALWAYS_ADD_MAIN_TARGET, PREFERENCE_PODFILE_ALLOW_PODS_IN_MULTIPLE_TARGETS, + PREFERENCE_ALLOW_EMPTY_PODFILE_GENERATION, PREFERENCE_SWIFT_PACKAGE_MANAGER_ENABLED }; @@ -1172,6 +1177,18 @@ public static bool PodfileAllowPodsInMultipleTargets { } } + /// + /// Whether to allow empty Podfile generation. True by default. + /// If true, a Podfile will be generated even if there are no Pods to install. + /// + public static bool AllowEmptyPodfileGeneration { + get { return settings.GetBool(PREFERENCE_ALLOW_EMPTY_PODFILE_GENERATION, + defaultValue: true); } + set { + settings.SetBool(PREFERENCE_ALLOW_EMPTY_PODFILE_GENERATION, value); + } + } + /// /// Whether to use Swift Package Manager for dependency resolution. /// If enabled, the resolver will attempt to use SPM for packages that define SPM support, @@ -1315,7 +1332,7 @@ public static bool PodPresent(string pod) { private static bool InjectDependencies() { return (EditorUserBuildSettings.activeBuildTarget == BuildTarget.iOS || EditorUserBuildSettings.activeBuildTarget == BuildTarget.tvOS) && - Enabled && (pods.Count > 0 || spmDependencies.SwiftPackages.Count > 0); + Enabled && (pods.Count > 0 || spmDependencies.SwiftPackages.Count > 0 || AllowEmptyPodfileGeneration); } /// @@ -2193,6 +2210,7 @@ public static void OnPostProcessResolveSwiftPackages(BuildTarget buildTarget, public static void OnPostProcessGenPodfile(BuildTarget buildTarget, string pathToBuiltProject) { if (!InjectDependencies() || !PodfileGenerationEnabled) return; + if (pods.Count == 0 && !AllowEmptyPodfileGeneration) return; GenPodfile(buildTarget, pathToBuiltProject, podsToIgnore); } @@ -2363,7 +2381,7 @@ public static void GenPodfile(BuildTarget buildTarget, } filteredPods.Add(pod); } - if (filteredPods.Count == 0) { + if (filteredPods.Count == 0 && !AllowEmptyPodfileGeneration) { Log("Found no pods to add, skipping generation of the Podfile"); podfileGenerationSkipped = true; return; diff --git a/source/IOSResolver/src/IOSResolverSettingsDialog.cs b/source/IOSResolver/src/IOSResolverSettingsDialog.cs index c0f4412b..756bc29f 100644 --- a/source/IOSResolver/src/IOSResolverSettingsDialog.cs +++ b/source/IOSResolver/src/IOSResolverSettingsDialog.cs @@ -44,6 +44,7 @@ private class Settings { internal bool podfileAlwaysAddMainTarget; internal bool podfileAllowPodsInMultipleTargets; internal bool swiftPackageManagerEnabled; + internal bool allowEmptyPodfileGeneration; internal bool useProjectSettings; internal EditorMeasurement.Settings analyticsSettings; @@ -66,6 +67,7 @@ internal Settings() { podfileAlwaysAddMainTarget = IOSResolver.PodfileAlwaysAddMainTarget; podfileAllowPodsInMultipleTargets = IOSResolver.PodfileAllowPodsInMultipleTargets; swiftPackageManagerEnabled = IOSResolver.SwiftPackageManagerEnabled; + allowEmptyPodfileGeneration = IOSResolver.AllowEmptyPodfileGeneration; useProjectSettings = IOSResolver.UseProjectSettings; analyticsSettings = new EditorMeasurement.Settings(IOSResolver.analytics); } @@ -89,6 +91,7 @@ internal void Save() { IOSResolver.PodfileAlwaysAddMainTarget = podfileAlwaysAddMainTarget; IOSResolver.PodfileAllowPodsInMultipleTargets = podfileAllowPodsInMultipleTargets; IOSResolver.SwiftPackageManagerEnabled = swiftPackageManagerEnabled; + IOSResolver.AllowEmptyPodfileGeneration = allowEmptyPodfileGeneration; IOSResolver.UseProjectSettings = useProjectSettings; analyticsSettings.Save(); } @@ -162,6 +165,14 @@ public void OnGUI() { "fall back to Cocoapods for any dependencies that do not have an " + "SPM equivalent specified."); + GUILayout.BeginHorizontal(); + GUILayout.Label("Allow empty Podfile generation", EditorStyles.boldLabel); + settings.allowEmptyPodfileGeneration = + EditorGUILayout.Toggle(settings.allowEmptyPodfileGeneration); + GUILayout.EndHorizontal(); + GUILayout.Label("If enabled, a Podfile will be generated even if there are no Pods " + + "to install."); + GUILayout.Box("", GUILayout.ExpandWidth(true), GUILayout.Height(1)); GUILayout.BeginHorizontal(); @@ -378,6 +389,9 @@ public void OnGUI() { new KeyValuePair( "swiftLanguageVersion", IOSResolver.SwiftLanguageVersion.ToString()), + new KeyValuePair( + "allowEmptyPodfileGeneration", + IOSResolver.AllowEmptyPodfileGeneration.ToString()), }, "Settings Save"); settings.Save();