protected async Task Sign(IEnumerable <string> paths, TaskToken task) { foreach (var path in paths) { await Sign(path, task); } }
protected async Task Sign(string path, TaskToken task, string entitlementsPath = null) { // Delete .meta files Unity might have erroneously copied to the build // and which will cause the signing to fail. // See: https://issuetracker.unity3d.com/issues/macos-standalone-build-contains-meta-files-inside-native-plugin-bundles if (Directory.Exists(path)) { var metas = Directory.GetFiles(path, "*.meta", SearchOption.AllDirectories); foreach (var meta in metas) { File.Delete(meta); } } var entitlements = ""; if (entitlementsPath != null) { entitlements = $" --entitlements '{entitlementsPath}'"; } var args = $"--force" + $" --deep" + $" --timestamp" + $" --options=runtime" + entitlements + $" --sign '{appSignIdentity}'" + $" '{path}'"; await Execute(new ExecutionArgs("codesign", args), task); }
protected async Task <bool> Staple(string path, bool silentError, TaskToken task) { var args = $"stapler staple '{path}'"; var code = await Execute(new ExecutionArgs("xcrun", args) { silentError = silentError }, task); return(code == 0); }
protected async Task <string> Upload(string path, TaskToken task) { var asc = ""; if (!string.IsNullOrEmpty(ascProvider)) { asc = $" --asc-provider '{ascProvider}'"; } var args = $"altool" + $" --notarize-app" + $" --primary-bundle-id '{primaryBundleId}'" + $" --username '{ascLogin.User}'" + asc + $" --file '{path}'"; string requestUUID = null; await Execute(new ExecutionArgs("xcrun", args) { input = ascLogin.GetPassword(keychainService) + "\n", onOutput = (output) => { if (requestUUID != null) { return; } var match = RequestUUIDRegex.Match(output); if (match.Success) { requestUUID = match.Groups[1].Value; } } }, task); return(requestUUID); }
protected async Task Upload(BuildPath zipPath, TaskToken task) { var archive = zipPath.path; if (!File.Exists(archive)) { throw new System.Exception("UploadDistro: Archive file does not exist: " + archive); } // Append a / to the url if necessary, otherwise curl treats the last part as a file name var url = uploadUrl; if (url[url.Length - 1] != '/') { url += "/"; } string input = null; if (!string.IsNullOrEmpty(login.User)) { input = string.Format("-u \"{0}:{1}\"", login.User, login.GetPassword(keychainService)); } var arguments = string.Format( "-T '{0}' {1} --ssl -v '{2}'", archive, input != null ? "-K -" : "", url ); task.Report(0, $"Uploading {Path.GetFileName(archive)} to {uploadUrl}"); await Execute(new ExecutionArgs(curlPath, arguments) { input = input }, task); }
protected async Task Zip(string input, string output, TaskToken task) { var args = $" -qr" + $" '{output}'" + $" '{input}'"; await Execute(new ExecutionArgs("zip", args), task); }
/// <summary> /// Run the distribution as an async task, /// since async tasks can't survive domain reloads, /// this method cannot build and will error if any builds are missing. /// </summary> public async Task DistributeWithoutBuilding(TaskToken task = default) { var removeProgressTask = false; if (task.taskId == 0) { // Set up default cancelable progress task if none is given removeProgressTask = true; task.taskId = Progress.Start(name); var source = new CancellationTokenSource(); task.cancellation = source.Token; Progress.RegisterCancelCallback(task.taskId, () => { source.Cancel(); return(true); }); } try { // Prevent domain reloads from stopping the distribution EditorApplication.LockReloadAssemblies(); // Check that all builds are present var paths = GetBuildPaths(); if (!paths.Any()) { throw new Exception(name + ": Distribution has no targets in any of its profiles"); } var missingBuilds = false; foreach (var path in paths) { if (path.path == null) { Debug.LogError(name + ": Missing build for target '" + path.target + "' of profile '" + path.profile + "'"); missingBuilds = true; } } if (missingBuilds) { throw new Exception($"{name}: Missing builds"); } // Run distribution await RunDistribute(paths, task); } finally { EditorApplication.UnlockReloadAssemblies(); if (removeProgressTask) { task.Remove(); } } }
/// <summary> /// Notarize a macOS build. /// </summary> /// <remarks> /// This method will silently ignore non-macOS builds. /// </remarks> /// <param name="buildPath">Build path</param> public async Task NotarizeIfMac(BuildPath buildPath, TaskToken task) { if (buildPath.target == BuildTarget.StandaloneOSX) { var child = task.StartChild("Notarize macOS Build"); try { await Notarize(buildPath, child); } finally { child.Remove(); } } }
protected async Task Upload(string archivePath, string exportOptionsPlist, TaskToken task) { var args = $"-exportArchive" + $" -archivePath '{archivePath}'" + $" -exportOptionsPlist '{exportOptionsPlist}'"; if (allowProvisioningUpdates) { args += " -allowProvisioningUpdates"; } await Execute(new ExecutionArgs("xcodebuild", args), task); }
protected async Task Process(string path, TaskToken task) { task.Report(0, 2); // Create archive var projectPath = Path.Combine(path, "Unity-iPhone.xcodeproj"); if (!Directory.Exists(projectPath)) { throw new Exception($"iOSDistro: Could not find Xcode project at path '{projectPath}'"); } var archiveName = $"{scheme}-{PlayerSettings.iOS.buildNumber}-{DateTime.Now.ToString("O")}.xcarchive"; var archivePath = Path.Combine(archivesPath, archiveName); task.Report(0, description: $"Building scheme '{scheme}'"); await Archive(projectPath, scheme, archivePath, task); // Upload archive var cleanUpOptions = false; string exportOptionsPath = null; try { if (exportOptions != null) { exportOptionsPath = AssetDatabase.GetAssetPath(exportOptions); } if (string.IsNullOrEmpty(exportOptionsPath)) { cleanUpOptions = true; exportOptionsPath = Path.GetTempFileName(); File.WriteAllText(exportOptionsPath, DefaultExportOptions); } task.Report(1, description: $"Uploading archive"); await Upload(archivePath, exportOptionsPath, task); } finally { if (cleanUpOptions) { File.Delete(exportOptionsPath); } } }
/// <summary> /// Async wrapper for OptionHelper.RunScriptAsync. /// </summary> protected async Task <int> Execute(ExecutionArgs args, TaskToken task) { var outputBuilder = new StringBuilder(); var errorBuilder = new StringBuilder(); int?exitcode = null; var terminator = OptionHelper.RunScriptAsnyc( args.startInfo, args.input, (output) => { outputBuilder.AppendLine(output); args.onOutput?.Invoke(output); }, (error) => { errorBuilder.AppendLine(error); args.onError?.Invoke(error); }, (code) => { exitcode = code; } ); while (exitcode == null) { if (task.cancellation.IsCancellationRequested) { terminator(true); task.ThrowIfCancellationRequested(); } await Task.Yield(); } // 137 happens for Kill() and 143 for CloseMainWindow(), // which means the script has been canceled if (!args.silentError && exitcode != 0 && exitcode != 137 && exitcode != 143) { throw new Exception(string.Format( "{0}: Failed to execute {1} (code {2}): {3}\nOutput: {4}", name, Path.GetFileName(args.startInfo.FileName), exitcode, errorBuilder.ToString(), outputBuilder.ToString() )); } return(exitcode.Value); }
protected async Task <string> WaitForCompletion(string requestUUID, TaskToken task) { string status = null, logFile = null; do { await Task.Delay(TimeSpan.FromSeconds(statusCheckInterval)); var args = $"altool" + $" --notarization-info '{requestUUID}'" + $" --username '{ascLogin.User}'"; status = null; await Execute(new ExecutionArgs("xcrun", args) { input = ascLogin.GetPassword(keychainService) + "\n", onOutput = (output) => { if (status == null) { var match = StatusRegex.Match(output); if (match.Success) { status = match.Groups[1].Value; } } if (logFile == null) { var match = LogFileRegex.Match(output); if (match.Success) { logFile = match.Groups[1].Value; } } } }, task); } while (status == "in progress"); if (logFile != null) { Debug.Log("Notarization log file: " + logFile); } return(status); }
protected async Task Upload(string path, TaskToken task) { var asc = ""; if (!string.IsNullOrEmpty(ascProvider)) { asc = $" --asc-provider '{ascProvider}'"; } var args = $"altool" + $" --upload-app" + $" --type osx" + $" --username '{ascLogin.User}'" + asc + $" --file '{path}'"; await Execute(new ExecutionArgs("xcrun", args) { input = ascLogin.GetPassword(keychainService) + "\n" }, task); }
public void Run(Job[] jobs, bool restoreActiveBuildTarget = true, UnityEngine.Object context = null) { if (jobs == null || jobs.Length == 0) { DestroyImmediate(this); throw new Exception($"Trimmer BuildRunner: No jobs given."); } EnsureNotRunning(); var results = new ProfileBuildResult[jobs.Length]; for (int i = 0; i < jobs.Length; i++) { var job = jobs[i]; if ((job.profile == null || job.target == 0 || job.target == BuildTarget.NoTarget) && job.distro == null) { DestroyImmediate(this); throw new Exception($"Trimmer BuildRunner: Invalid job at index {i}: Profile or target or distro not set ({job.profile} / {job.target} / {job.distro})"); } results[i].profile = job.profile; } Current = this; this.jobs = jobs; this.results = results; restoreActiveTargetTo = EditorUserBuildSettings.activeBuildTarget; jobIndex = -1; token = TaskToken.Start(context?.name ?? "Trimmer", options: Progress.Options.Synchronous); token.context = context; Progress.SetPriority(token.taskId, Progress.Priority.High); token.Report(0, jobs.Length); //Debug.Log($"Trimmer BuildRunner: Got jobs:\n{string.Join("\n", jobs.Select(j => $"- {j.profile?.name ?? "<none>"} {j.target}"))}"); ContinueWith(ContinueTask.NextJob); }
async Task Distribute(BuildPath buildPath, TaskToken task) { task.Report(0, description: $"Pushing {buildPath.target}"); var path = OptionHelper.GetBuildBasePath(buildPath.path); var channel = ChannelNames[buildPath.target]; if (!string.IsNullOrEmpty(channelSuffix)) { channel += "-" + channelSuffix; } var version = Application.version; var buildInfo = BuildInfo.FromPath(path); if (buildInfo != null) { if (!buildInfo.version.IsDefined) { Debug.LogWarning("ItchDistro: build.json exists but contains no version."); } else { version = buildInfo.version.MajorMinorPatchBuild; } } var args = string.Format( "push '{0}' '{1}:{2}' --userversion '{3}' --ignore='*.DS_Store' --ignore='build.json'", path, project, channel, Application.version ); await Execute(new ExecutionArgs(butlerPath, args), task); }
protected override async Task RunDistribute(IEnumerable <BuildPath> buildPaths, TaskToken task) { if (string.IsNullOrEmpty(curlPath)) { throw new Exception("UploadDistro: Path to curl not set."); } if (!File.Exists(curlPath)) { throw new Exception("UploadDistro: curl not found at path: " + curlPath); } if (string.IsNullOrEmpty(uploadUrl)) { throw new Exception("UploadDistro: No upload URL set."); } if (!string.IsNullOrEmpty(login.User) && login.GetPassword(keychainService) == null) { throw new Exception("UploadDistro: No password set for user: "******"Archiving builds"); IEnumerable <BuildPath> zipPaths; var child = task.StartChild("Zip Builds"); try { zipPaths = await ZipBuilds(buildPaths, child); } finally { child.Remove(); } task.Report(1, description: "Uploading builds"); child = task.StartChild("Upload Builds"); try { foreach (var path in zipPaths) { await Upload(path, child); } } finally { child.Remove(); } }
protected async Task <IEnumerable <BuildPath> > ZipBuilds(IEnumerable <BuildPath> buildPaths, TaskToken task) { var queue = new Queue <BuildPath>(buildPaths); var results = new List <BuildPath>(); task.Report(0, queue.Count); while (queue.Count > 0) { var next = queue.Dequeue(); if (macNotarization != null) { await macNotarization.NotarizeIfMac(next, task); } results.Add(await Zip(next, task)); task.baseStep++; } return(results); }
protected override async Task RunDistribute(IEnumerable <BuildPath> buildPaths, TaskToken task) { foreach (var buildPath in buildPaths) { if (buildPath.target != BuildTarget.StandaloneOSX) { continue; } await Notarize(buildPath, task); } }
protected override async Task RunDistribute(IEnumerable <BuildPath> buildPaths, TaskToken task) { // Check Pipeline Builder Executable var cmd = FindPipelineBuilder(); // Check User if (string.IsNullOrEmpty(gogLogin.User)) { throw new Exception("GOGDistro: No GOG user set."); } if (gogLogin.GetPassword(keychainService) == null) { throw new Exception("GOGDistro: No GOG password found in Keychain."); } // Check projects if (string.IsNullOrEmpty(projectsFolder) || !Directory.Exists(projectsFolder)) { throw new Exception("GOGDistro: Path to projects folder not set."); } // Check ignore list if (!string.IsNullOrEmpty(ignoreList) && !File.Exists(ignoreList)) { throw new Exception("GOGDistro: Ignore list could not be found: " + ignoreList); } // Process projects var tempDir = FileUtil.GetUniqueTempPathInProject(); try { Directory.CreateDirectory(tempDir); var targets = new HashSet <BuildTarget>(buildPaths.Select(p => p.target)); var projects = new List <string>(); string convertError = null; foreach (var file in Directory.GetFiles(projectsFolder)) { if (Path.GetExtension(file).ToLower() != ".json") { continue; } var contents = PathVarRegex.Replace(File.ReadAllText(file), (match) => { var platformName = match.Groups[1].Value.ToLower(); if (platformName == "project") { return(Path.GetDirectoryName(Application.dataPath)); } else if (platformName == "projects") { return(Path.GetFullPath(projectsFolder)); } BuildTarget target; try { target = (BuildTarget)System.Enum.Parse(typeof(BuildTarget), platformName, true); } catch { convertError = $"Invalid build target path variable '{platformName}' in project JSON: {file}"; return(""); } if (!buildPaths.Any(p => p.target == target)) { convertError = $"Build target '{platformName}' not part of given build profile(s) in project JSON: {file}"; return(""); } targets.Remove(target); var path = buildPaths.Where(p => p.target == target).Select(p => p.path).First(); path = OptionHelper.GetBuildBasePath(path); return(Path.GetFullPath(path)); }); if (convertError != null) { break; } var targetPath = Path.Combine(tempDir, Path.GetFileName(file)); File.WriteAllText(targetPath, contents); projects.Add(targetPath); } if (convertError != null) { throw new Exception($"GOGDistro: {convertError}"); } if (targets.Count > 0) { Debug.LogWarning("GOGDistro: Not all build targets filled into variables. Left over: " + string.Join(", ", targets.Select(t => t.ToString()).ToArray())); } task.Report(0, targets.Count + 1); // Notarize mac builds if (macNotarization != null) { task.Report(0, description: "Notarizing macOS builds"); foreach (var path in buildPaths.Where(p => p.target == BuildTarget.StandaloneOSX)) { await macNotarization.Notarize(path, task); } } // Build task.baseStep++; foreach (var project in projects) { var args = string.Format( "build-game '{0}' --username='******' --password='******' --version={3}", Path.GetFullPath(project), gogLogin.User, gogLogin.GetPassword(keychainService), string.IsNullOrEmpty(overrideVersion) ? Application.version : overrideVersion ); if (!string.IsNullOrEmpty(ignoreList)) { args += $" --ignore_list='{Path.GetFullPath(ignoreList)}'"; } if (!string.IsNullOrEmpty(branch.User)) { args += $" --branch='{branch.User}'"; var pwd = branch.GetPassword(branchKeychainService); if (pwd != null) { args += $" --branch_password='******'"; } } task.Report(0, description: $"Uploading {Path.GetFileName(project)}"); await Execute(new ExecutionArgs(cmd, args), task); task.baseStep++; } } finally { // Always clean up temporary files Directory.Delete(tempDir, true); } }
protected override async Task RunDistribute(IEnumerable <BuildPath> buildPaths, TaskToken task) { var hasMacBuild = false; foreach (var buildPath in buildPaths) { if (buildPath.target != BuildTarget.StandaloneOSX) { continue; } hasMacBuild = true; await Process(buildPath.path, task); } if (!hasMacBuild) { throw new Exception("MASDistro: No macOS build in profiles"); } }
protected async Task Process(string path, TaskToken task) { // Check settings if (string.IsNullOrEmpty(appSignIdentity)) { throw new Exception("MASDistro: App sign identity not set."); } if (entitlements == null) { throw new Exception("MASDistro: Entitlements file not set."); } if (provisioningProfile == null) { throw new Exception("MASDistro: Provisioning profile not set."); } if (linkFrameworks != null && linkFrameworks.Length > 0 && !File.Exists(optoolPath)) { throw new Exception("MASDistro: optool path not set for linking frameworks."); } var plistPath = Path.Combine(path, "Contents/Info.plist"); if (!File.Exists(plistPath)) { throw new Exception("MASDistro: Info.plist file not found at path: " + plistPath); } task.Report(0, 3); var doc = new PlistDocument(); doc.ReadFromFile(plistPath); // Edit Info.plist if (!string.IsNullOrEmpty(copyright) || !string.IsNullOrEmpty(languages)) { if (!string.IsNullOrEmpty(copyright)) { doc.root.SetString("NSHumanReadableCopyright", string.Format(copyright, System.DateTime.Now.Year)); } if (!string.IsNullOrEmpty(languages)) { var parts = languages.Split(','); var array = doc.root.CreateArray("CFBundleLocalizations"); foreach (var part in parts) { array.AddString(part.Trim()); } } doc.WriteToFile(plistPath); } // Link frameworks if (linkFrameworks != null && linkFrameworks.Length > 0) { task.Report(0, description: "Linking frameworks"); var binaryPath = Path.Combine(path, "Contents/MacOS"); binaryPath = Path.Combine(binaryPath, doc.root["CFBundleExecutable"].AsString()); foreach (var framework in linkFrameworks) { var frameworkBinaryPath = FindFramework(framework); if (frameworkBinaryPath == null) { throw new Exception("MASDistro: Could not locate framework: " + framework); } var otoolargs = string.Format( "install -c weak -p '{0}' -t '{1}'", frameworkBinaryPath, binaryPath ); await Execute(new ExecutionArgs(optoolPath, otoolargs), task); } } // Copy provisioning profile var profilePath = AssetDatabase.GetAssetPath(provisioningProfile); var embeddedPath = Path.Combine(path, "Contents/embedded.provisionprofile"); File.Copy(profilePath, embeddedPath, true); // Sign plugins var plugins = Path.Combine(path, "Contents/Plugins"); if (Directory.Exists(plugins)) { task.Report(0, description: "Signing plugins"); await Sign(Directory.GetFiles(plugins, "*.dylib", SearchOption.AllDirectories), task); await Sign(Directory.GetFiles(plugins, "*.bundle", SearchOption.AllDirectories), task); await Sign(Directory.GetDirectories(plugins, "*.bundle", SearchOption.TopDirectoryOnly), task); } // Sign application task.Report(1, description: "Singing app"); var entitlementsPath = AssetDatabase.GetAssetPath(entitlements); await Sign(path, task, entitlementsPath); // Create installer var pkgPath = Path.ChangeExtension(path, ".pkg"); if (!string.IsNullOrEmpty(installerSignIdentity)) { task.Report(1, description: "Creating installer"); var args = string.Format( "--component '{0}' /Applications --sign '{1}' '{2}'", path, installerSignIdentity, pkgPath ); await Execute(new ExecutionArgs("productbuild", args), task); } // Upload to App Store if (!string.IsNullOrEmpty(ascLogin.User)) { task.Report(2, description: "Uploading to App Store Connect"); await Upload(pkgPath, task); } }
protected override async Task RunDistribute(IEnumerable <BuildPath> buildPaths, TaskToken task) { var hasTarget = false; foreach (var buildPath in buildPaths) { if (buildPath.target != BuildTarget.iOS) { continue; } hasTarget = true; await Process(buildPath.path, task); } if (!hasTarget) { throw new Exception($"iOSDistro: No iOS build target in build profiles"); } }
protected async Task Archive(string projectPath, string schemeName, string archivePath, TaskToken task) { var args = $"archive" + $" -project '{projectPath}'" + $" -archivePath '{archivePath}'" + $" -scheme '{schemeName}'"; if (allowProvisioningUpdates) { args += " -allowProvisioningUpdates"; } await Execute(new ExecutionArgs("xcodebuild", args), task); }
protected override async Task RunDistribute(IEnumerable <BuildPath> buildPaths, TaskToken task) { if (string.IsNullOrEmpty(scriptPath)) { throw new Exception("ScriptDistro: Script path not set."); } if (individual) { task.Report(0, buildPaths.Count()); foreach (var buildPath in buildPaths) { task.Report(0, description: $"Running script {Path.GetFileName(scriptPath)} for {buildPath.target}"); var args = ReplaceVariablesIndividual(arguments, buildPath); await Execute(new ExecutionArgs(scriptPath, args), task); task.baseStep++; } } else { task.Report(0, 1, $"Running script {Path.GetFileName(scriptPath)}"); var args = ReplaceVariables(arguments, buildPaths); await Execute(new ExecutionArgs(scriptPath, args), task); } }
/// <summary> /// Subroutine to override in subclasses to do the actual processing. /// </summary> /// <param name="buildPaths">Build paths of the linked Build Profiles</param> /// <param name="task">Task token for reporting progress and cancellation</param> protected virtual Task RunDistribute(IEnumerable <BuildPath> buildPaths, TaskToken task) { return(Task.CompletedTask); }
/// <summary> /// Notarize a macOS build. /// </summary> /// <remarks> /// This method will throw if the given build is not a macOS build. /// </remarks> /// <param name="macBuildPath">Path to the app bundle</param> public async Task Notarize(BuildPath macBuildPath, TaskToken task) { if (macBuildPath.target != BuildTarget.StandaloneOSX) { throw new Exception($"NotarizationDistro: Notarization is only available for macOS builds (got {macBuildPath.target})"); } var path = macBuildPath.path; // Check settings if (string.IsNullOrEmpty(appSignIdentity)) { throw new Exception("NotarizationDistro: App sign identity not set."); } if (entitlements == null) { throw new Exception("NotarizationDistro: Entitlements file not set."); } // Check User if (string.IsNullOrEmpty(ascLogin.User)) { throw new Exception("NotarizationDistro: No App Store Connect user set."); } if (ascLogin.GetPassword(keychainService) == null) { throw new Exception("NotarizationDistro: No App Store Connect password found in Keychain."); } task.Report(0, 6, "Checking if app is already notarized"); // Try stapling in case the build has already been notarized if (await Staple(path, silentError: true, task)) { Debug.Log("Build already notarized, nothing more to do..."); return; } task.Report(1, description: "Signing app"); // Sign plugins // codesign --deep --force does not resign nested plugins, // --force only applies to the main bundle. If we want to // resign nested plugins, we have to call codesign for each. // This is required for library validation with the hardened runtime. var plugins = Path.Combine(path, "Contents/Plugins"); if (Directory.Exists(plugins)) { await Sign(Directory.GetFiles(plugins, "*.dylib", SearchOption.TopDirectoryOnly), task); await Sign(Directory.GetFiles(plugins, "*.bundle", SearchOption.TopDirectoryOnly), task); await Sign(Directory.GetDirectories(plugins, "*.bundle", SearchOption.TopDirectoryOnly), task); } // Sign application var entitlementsPath = AssetDatabase.GetAssetPath(entitlements); await Sign(path, task, entitlementsPath); task.Report(2, description: "Zipping app"); // Zip app var zipPath = path + ".zip"; await Zip(path, zipPath, task); task.Report(3, description: "Uploading app"); // Upload for notarization string requestUUID = null; try { requestUUID = await Upload(zipPath, task); if (requestUUID == null) { throw new Exception("NotarizationDistro: Could not parse request UUID from upload output"); } } finally { // Delete ZIP regardless of upload result File.Delete(zipPath); } task.Report(4, description: "Waiting for notarization result"); // Wait for notarization to complete var status = await WaitForCompletion(requestUUID, task); if (status != "success") { throw new Exception($"NotarizationDistro: Got '{status}' notarization status"); } task.Report(5, description: "Stapling ticket to app"); // Staple await Staple(path, silentError : false, task); }
protected override async Task RunDistribute(IEnumerable <BuildPath> buildPaths, TaskToken task) { await ZipBuilds(buildPaths, task); }
protected async Task RenameRoot(string archivePath, string oldName, string newName, TaskToken task) { if (!File.Exists(archivePath)) { throw new Exception("ZipDistro: Path to archive does not exist: " + archivePath); } var args = string.Format( "rn '{0}' '{1}' '{2}'", archivePath, oldName, newName ); await Execute(new ExecutionArgs(Get7ZipPath(), args), task); }
protected async Task <BuildPath> Zip(BuildPath buildPath, TaskToken task) { var target = buildPath.target; var path = buildPath.path; if (!File.Exists(path) && !Directory.Exists(path)) { throw new Exception("ZipDistro: Path to compress does not exist: " + path); } if (ZipIgnorePatterns == null) { ZipIgnorePatterns = new Regex[ZipIgnore.Length]; for (int i = 0; i < ZipIgnore.Length; i++) { var regex = Regex.Escape(ZipIgnore[i]).Replace(@"\*", ".*").Replace(@"\?", "."); ZipIgnorePatterns[i] = new Regex(regex); } } var sevenZPath = Get7ZipPath(); // Path can point to executable file but there might be files // in the containing directory we need as well var basePath = OptionHelper.GetBuildBasePath(path); // Check the files in containing directory var files = new List <string>(Directory.GetFileSystemEntries(basePath)); for (int i = files.Count - 1; i >= 0; i--) { var filename = Path.GetFileName(files[i]); foreach (var pattern in ZipIgnorePatterns) { if (pattern.IsMatch(filename)) { files.RemoveAt(i); goto ContinueOuter; } } ContinueOuter :; } if (files.Count == 0) { throw new Exception("ZipDistro: Nothing to ZIP in directory: " + basePath); } // Determine output path first to make it consistent and use absolute path // since the script will be run in a different working directory var prettyName = GetPrettyName(target); if (prettyName == null) { prettyName = Path.GetFileNameWithoutExtension(basePath); } var versionSuffix = ""; if (appendVersion) { var buildInfo = BuildInfo.FromPath(path); if (buildInfo != null) { if (!buildInfo.version.IsDefined) { Debug.LogWarning("ZipDistro: build.json exists but contains no version"); } else { versionSuffix = " " + buildInfo.version.MajorMinorPatch; } } if (versionSuffix.Length == 0) { versionSuffix = " " + Application.version; } } var extension = FileExtensions[(int)format]; var zipName = prettyName + versionSuffix + extension; zipName = zipName.Replace(" ", "_"); var outputPath = Path.Combine(Path.GetDirectoryName(basePath), zipName); outputPath = Path.GetFullPath(outputPath); // Delete existing archive, otherwise 7za will update it if (File.Exists(outputPath)) { File.Delete(outputPath); } // In case it only contains a single file, just zip that file var singleFile = false; if (files.Count == 1) { singleFile = true; basePath = files[0]; } // Run 7za command to create ZIP file var excludes = ""; foreach (var pattern in ZipIgnore) { excludes += @" -xr\!'" + pattern + "'"; } var inputName = Path.GetFileName(basePath); var args = string.Format( "a '{0}' '{1}' -r -mx{2} {3}", outputPath, inputName, (int)compression, excludes ); var startInfo = new System.Diagnostics.ProcessStartInfo(); startInfo.FileName = sevenZPath; startInfo.Arguments = args; startInfo.WorkingDirectory = Path.GetDirectoryName(basePath); task.Report(0, description: $"Archiving {inputName}"); await Execute(new ExecutionArgs() { startInfo = startInfo }, task); if (!singleFile && prettyName != inputName) { await RenameRoot(outputPath, inputName, prettyName, task); } return(new BuildPath(buildPath.profile, target, outputPath)); }
protected override async Task RunDistribute(IEnumerable <BuildPath> buildPaths, TaskToken task) { // Check SDK var cmd = FindSteamCmd(); // Check User if (string.IsNullOrEmpty(steamLogin.User)) { throw new Exception("SteamDistro: No Steam user set."); } if (steamLogin.GetPassword(keychainService) == null) { throw new Exception("SteamDistro: No Steam password found in Keychain."); } // Check scripts if (string.IsNullOrEmpty(scriptsFolder) || !Directory.Exists(scriptsFolder)) { throw new Exception("SteamDistro: Path to scripts folder not set."); } if (string.IsNullOrEmpty(appScript)) { throw new Exception("SteamDistro: Name of app script not set."); } var appScriptPath = Path.Combine(scriptsFolder, appScript); if (!File.Exists(appScriptPath)) { throw new Exception("SteamDistro: App script not found in scripts folder."); } // Process scripts var tempDir = FileUtil.GetUniqueTempPathInProject(); try { Directory.CreateDirectory(tempDir); var targets = new HashSet <BuildTarget>(buildPaths.Select(p => p.target)); string convertError = null; foreach (var file in Directory.GetFiles(scriptsFolder)) { if (Path.GetExtension(file).ToLower() != ".vdf") { continue; } var contents = PathVarRegex.Replace(File.ReadAllText(file), (match) => { var platformName = match.Groups[1].Value.ToLower(); if (platformName == "project") { return(Path.GetDirectoryName(Application.dataPath)); } else if (platformName == "scripts") { return(Path.GetFullPath(scriptsFolder)); } BuildTarget target; try { target = (BuildTarget)System.Enum.Parse(typeof(BuildTarget), platformName, true); } catch { convertError = $"SteamDistro: Invalid build target path variable '{platformName}' in VDF script: {file}"; return(""); } if (!buildPaths.Any(p => p.target == target)) { convertError = $"SteamDistro: Build target '{platformName}' not part of given build profile(s) in VDF script: {file}"; return(""); } targets.Remove(target); var path = buildPaths.Where(p => p.target == target).Select(p => p.path).First(); path = OptionHelper.GetBuildBasePath(path); return(Path.GetFullPath(path)); }); if (convertError != null) { break; } var targetPath = Path.Combine(tempDir, Path.GetFileName(file)); File.WriteAllText(targetPath, contents); } if (convertError != null) { throw new Exception($"SteamDistro: {convertError}"); } if (targets.Count > 0) { Debug.LogWarning("SteamDistro: Not all build targets filled into variables. Left over: " + string.Join(", ", targets.Select(t => t.ToString()).ToArray())); } // Notarize mac builds if (macNotarization != null) { task.Report(0, description: "Notarizing macOS builds"); foreach (var path in buildPaths.Where(p => p.target == BuildTarget.StandaloneOSX)) { await macNotarization.Notarize(path, task); } } // Build var scriptPath = Path.GetFullPath(Path.Combine(tempDir, appScript)); var args = string.Format( "+login '{0}' '{1}' +run_app_build_http '{2}' +quit", steamLogin.User, steamLogin.GetPassword(keychainService), scriptPath ); await Execute(new ExecutionArgs(cmd, args) { onOutput = (output) => { if (output.Contains("Logged in OK")) { task.Report(0, description: "Logged in"); } else if (output.Contains("Building depot")) { var match = BuildingDepotRegex.Match(output); if (match.Success) { task.Report(0, description: $"Building depo {match.Groups[1].Value}"); } } else if (output.Contains("")) { var match = SuccessBuildIdRegex.Match(output); if (match.Success) { Debug.Log("SteamDistro: Build uploaded, ID = " + match.Groups[1].Value); } } } }, task); } finally { // Always clean up temp files Directory.Delete(tempDir, true); } }