private static void Publish() { if (Directory.Exists(BinDistDirectory)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(BinDistDirectory).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {BinDistDirectory}"); } using (var process = Process.Start(new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = GuiProjectDirectory })) { process.StandardInput.WriteLine("dotnet clean --configuration Release && exit"); process.WaitForExit(); } var guiBinReleaseDirectory = Path.GetFullPath(Path.Combine(GuiProjectDirectory, "bin\\Release")); var libraryBinReleaseDirectory = Path.GetFullPath(Path.Combine(LibraryProjectDirectory, "bin\\Release")); if (Directory.Exists(guiBinReleaseDirectory)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(guiBinReleaseDirectory).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {guiBinReleaseDirectory}"); } if (Directory.Exists(libraryBinReleaseDirectory)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(libraryBinReleaseDirectory).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {libraryBinReleaseDirectory}"); } foreach (string target in Targets) { string publishedFolder = Path.Combine(BinDistDirectory, target); string currentBinDistDirectory = publishedFolder; Console.WriteLine(); Console.WriteLine($"{nameof(currentBinDistDirectory)}:\t{currentBinDistDirectory}"); Console.WriteLine(); if (!Directory.Exists(currentBinDistDirectory)) { Directory.CreateDirectory(currentBinDistDirectory); Console.WriteLine($"Created {currentBinDistDirectory}"); } using (var process = Process.Start(new ProcessStartInfo { FileName = "dotnet", Arguments = $"clean", WorkingDirectory = GuiProjectDirectory })) { process.WaitForExit(); } // https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish?tabs=netcore21 // -c|--configuration {Debug|Release} // Defines the build configuration. The default value is Debug. // --force // Forces all dependencies to be resolved even if the last restore was successful. Specifying this flag is the same as deleting the project.assets.json file. // -o|--output <OUTPUT_DIRECTORY> // Specifies the path for the output directory. // If not specified, it defaults to ./bin/[configuration]/[framework]/publish/ for a framework-dependent deployment or // ./bin/[configuration]/[framework]/[runtime]/publish/ for a self-contained deployment. // If the path is relative, the output directory generated is relative to the project file location, not to the current working directory. // --self-contained // Publishes the .NET Core runtime with your application so the runtime does not need to be installed on the target machine. // If a runtime identifier is specified, its default value is true. For more information about the different deployment types, see .NET Core application deployment. // -r|--runtime <RUNTIME_IDENTIFIER> // Publishes the application for a given runtime. This is used when creating a self-contained deployment (SCD). // For a list of Runtime Identifiers (RIDs), see the RID catalog. Default is to publish a framework-dependent deployment (FDD). // --version-suffix <VERSION_SUFFIX> // Defines the version suffix to replace the asterisk (*) in the version field of the project file. // https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-restore?tabs=netcore2x // --disable-parallel // Disables restoring multiple projects in parallel. // --no-cache // Specifies to not cache packages and HTTP requests. // https://github.com/dotnet/docs/issues/7568 // /p:Version=1.2.3.4 // "dotnet publish" supports msbuild command line options like /p:Version=1.2.3.4 using (var process = Process.Start(new ProcessStartInfo { FileName = "dotnet", Arguments = $"publish --configuration Release --force --output \"{currentBinDistDirectory}\" --self-contained true --runtime \"{target}\" /p:VersionPrefix={VersionPrefix} --disable-parallel --no-cache /p:DebugType=none /p:DebugSymbols=false /p:ErrorReport=none /p:DocumentationFile=\"\" /p:Deterministic=true", WorkingDirectory = GuiProjectDirectory })) { process.WaitForExit(); } Tools.ClearSha512Tags(currentBinDistDirectory); //Tools.RemoveSosDocsUnix(currentBinDistDirectory); // Remove Tor binaries that are not relevant to the platform. var torFolder = new DirectoryInfo(Path.Combine(currentBinDistDirectory, "TorDaemons")); var toNotRemove = ""; if (target.StartsWith("win")) { toNotRemove = "win"; } else if (target.StartsWith("linux")) { toNotRemove = "lin"; } else if (target.StartsWith("osx")) { toNotRemove = "osx"; } foreach (var file in torFolder.EnumerateFiles()) { if (!file.Name.Contains("data", StringComparison.OrdinalIgnoreCase) && !file.Name.Contains(toNotRemove, StringComparison.OrdinalIgnoreCase)) { File.Delete(file.FullName); } } // Remove binaries that are not relevant to the platform. var binaryFolder = new DirectoryInfo(Path.Combine(currentBinDistDirectory, "Microservices", "Binaries")); foreach (var dir in binaryFolder.EnumerateDirectories()) { if (!dir.Name.Contains(toNotRemove, StringComparison.OrdinalIgnoreCase)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(dir.FullName).GetAwaiter().GetResult(); } } // Rename the final exe. string oldExecutablePath; string newExecutablePath; if (target.StartsWith("win")) { oldExecutablePath = Path.Combine(currentBinDistDirectory, "WalletWasabi.Gui.exe"); newExecutablePath = Path.Combine(currentBinDistDirectory, $"{ExecutableName}.exe"); } else // Linux & OSX { oldExecutablePath = Path.Combine(currentBinDistDirectory, "WalletWasabi.Gui"); newExecutablePath = Path.Combine(currentBinDistDirectory, ExecutableName); } File.Move(oldExecutablePath, newExecutablePath); long installedSizeKb = Tools.DirSize(new DirectoryInfo(publishedFolder)) / 1000; if (target.StartsWith("win")) { var icoPath = Path.Combine(GuiProjectDirectory, "Assets", "WasabiLogo.ico"); using (var process = Process.Start(new ProcessStartInfo { FileName = "rcedit", // https://github.com/electron/rcedit/ Arguments = $"\"{newExecutablePath}\" --set-icon \"{icoPath}\" --set-file-version \"{VersionPrefix}\" --set-product-version \"{VersionPrefix}\" --set-version-string \"LegalCopyright\" \"MIT\" --set-version-string \"CompanyName\" \"zkSNACKs\" --set-version-string \"FileDescription\" \"Privacy focused, ZeroLink compliant Bitcoin wallet.\" --set-version-string \"ProductName\" \"Wasabi Wallet\"", WorkingDirectory = currentBinDistDirectory })) { process.WaitForExit(); } var daemonExePath = newExecutablePath.Substring(0, newExecutablePath.Length - 4) + "d.exe"; File.Copy(newExecutablePath, daemonExePath); // Do not open console. if (!NSubsysUtil.ProcessFile(newExecutablePath)) { Console.WriteLine("ERROR: Could not remove console from exe."); } // IF IT'S IN ONLYBINARIES MODE DON'T DO ANYTHING FANCY PACKAGING AFTER THIS!!! if (OnlyBinaries) { continue; // In Windows build at this moment it does not matter though. } } else if (target.StartsWith("osx")) { // IF IT'S IN ONLYBINARIES MODE DON'T DO ANYTHING FANCY PACKAGING AFTER THIS!!! if (OnlyBinaries) { continue; } var tempName = Path.Combine(BinDistDirectory, $"temp-{target}"); Directory.Move(currentBinDistDirectory, tempName); currentBinDistDirectory = tempName; string macWasabiAppDir = Path.Combine(publishedFolder, "Wasabi Wallet.App"); // This should be lowercase .app, but MAC will prevent people from upgrading if we change it. string macContentsDir = Path.Combine(macWasabiAppDir, "Contents"); string newName = Path.GetFullPath(Path.Combine(macContentsDir, "MacOS")); Directory.CreateDirectory(macContentsDir); Directory.Move(currentBinDistDirectory, newName); currentBinDistDirectory = newName; string resourcesDir = Path.Combine(macContentsDir, "Resources"); string infoFilePath = Path.Combine(macContentsDir, "Info.plist"); Directory.CreateDirectory(resourcesDir); var iconPath = Path.Combine(GuiProjectDirectory, "Assets", "WasabiLogo.icns"); File.Copy(iconPath, Path.Combine(resourcesDir, "WasabiLogo.icns")); string infoContent = $@"<?xml version=""1.0"" encoding=""UTF-8""?> <!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd""> <plist version = ""1.0""> <dict> <key>LSMinimumSystemVersion</key> <string>10.12</string> <key>LSArchitecturePriority</key> <array> <string>x86_64</string> </array> <key>CFBundleIconFile</key> <string>WasabiLogo.icns</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>{VersionPrefix}</string> <key>CFBundleVersion</key> <string>{VersionPrefix}</string> <key>CFBundleExecutable</key> <string>{ExecutableName}</string> <key>CFBundleName</key> <string>Wasabi Wallet</string> <key>CFBundleIdentifier</key> <string>zksnacks.wasabiwallet</string> <key>NSHighResolutionCapable</key> <true/> <key>NSAppleScriptEnabled</key> <true/> <key>LSApplicationCategoryType</key> <string>public.app-category.finance</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> </dict> </plist> "; File.WriteAllText(infoFilePath, infoContent); using (var process = Process.Start(new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = publishedFolder })) { process.StandardInput.WriteLine($"wsl ln -s /Applications && exit"); process.WaitForExit(); } //how to generate .DS_Store file - https://github.com/zkSNACKs/WalletWasabi/pull/928/commits/e38ed672dee25f6e45a3eb16584887cc6d48c4e6 var dmgContentDir = Path.Combine(PackagerProjectDirectory, "Content", "Osx"); IoHelpers.CopyFilesRecursively(new DirectoryInfo(dmgContentDir), new DirectoryInfo(publishedFolder)); string uncompressedDmgFileName = $"Wasabi-uncompressed.dmg"; string uncompressedDmgFilePath = Path.Combine(BinDistDirectory, uncompressedDmgFileName); string dmgFileName = $"Wasabi-{VersionPrefix}.dmg"; using (var process = Process.Start(new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = BinDistDirectory })) { // http://www.nathancoulson.com/proj_cross_tools.php // -D: Do not use deep directory relocation, and instead just pack them in the way we see them // -V: Volume Label // -no-pad: Do not pad the end by 150 sectors (300kb). As it is not a cd image, not required // -apple -r: Creates a .dmg image process.StandardInput.WriteLine($"wsl genisoimage -D -V \"Wasabi Wallet\" -no-pad -apple -r -dir-mode 755 -o \"{uncompressedDmgFileName}\" \"{new DirectoryInfo(publishedFolder).Name}\" && exit"); process.WaitForExit(); } // cd ~ // git clone https://github.com/planetbeing/libdmg-hfsplus.git && cd libdmg-hfsplus // https://github.com/planetbeing/libdmg-hfsplus/issues/14 // mkdir build && cd build // sudo apt-get install zlib1g-dev // cmake .. // cd build // sudo apt-get install libssl1.0-dev // cmake .. // cd ~/libdmg-hfsplus/build/ // make using (var process = Process.Start(new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = BinDistDirectory })) { process.StandardInput.WriteLine($"wsl ~/libdmg-hfsplus/build/dmg/./dmg dmg \"{uncompressedDmgFileName}\" \"{dmgFileName}\" && exit"); process.WaitForExit(); } IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); File.Delete(uncompressedDmgFilePath); IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } else if (target.StartsWith("linux")) { // IF IT'S IN ONLYBINARIES MODE DON'T DO ANYTHING FANCY PACKAGING AFTER THIS!!! if (OnlyBinaries) { continue; } Console.WriteLine("Create Linux .tar.gz"); if (!Directory.Exists(publishedFolder)) { throw new Exception($"{publishedFolder} does not exist."); } var newFolderName = $"WasabiLinux-{VersionPrefix}"; var newFolderPath = Path.Combine(BinDistDirectory, newFolderName); Directory.Move(publishedFolder, newFolderPath); publishedFolder = newFolderPath; var linuxPath = $"/mnt/c/{Tools.LinuxPath(BinDistDirectory.Replace("C:\\", ""))}"; // We assume that it is on drive C:\. var commands = new[] { "cd ~", "sudo umount /mnt/c", "sudo mount -t drvfs C: /mnt/c -o metadata", $"cd {linuxPath}", $"sudo find ./{newFolderName} -type f -exec chmod 644 {{}} \\;", $"sudo find ./{newFolderName} -type f \\( -name 'wassabee' -o -name 'hwi' -o -name 'bitcoind' \\) -exec chmod +x {{}} \\;", $"tar -pczvf {newFolderName}.tar.gz {newFolderName}" }; string arguments = string.Join(" && ", commands); using (var process = Process.Start(new ProcessStartInfo { FileName = "wsl", Arguments = arguments, RedirectStandardInput = true, WorkingDirectory = BinDistDirectory })) { process.WaitForExit(); } Console.WriteLine("Create Linux .deb"); var debFolderRelativePath = "deb"; var debFolderPath = Path.Combine(BinDistDirectory, debFolderRelativePath); var linuxUsrLocalBinFolder = "/usr/local/bin/"; var debUsrLocalBinFolderRelativePath = Path.Combine(debFolderRelativePath, "usr", "local", "bin"); var debUsrLocalBinFolderPath = Path.Combine(BinDistDirectory, debUsrLocalBinFolderRelativePath); Directory.CreateDirectory(debUsrLocalBinFolderPath); var debUsrAppFolderRelativePath = Path.Combine(debFolderRelativePath, "usr", "share", "applications"); var debUsrAppFolderPath = Path.Combine(BinDistDirectory, debUsrAppFolderRelativePath); Directory.CreateDirectory(debUsrAppFolderPath); var debUsrShareIconsFolderRelativePath = Path.Combine(debFolderRelativePath, "usr", "share", "icons", "hicolor"); var debUsrShareIconsFolderPath = Path.Combine(BinDistDirectory, debUsrShareIconsFolderRelativePath); var debianFolderRelativePath = Path.Combine(debFolderRelativePath, "DEBIAN"); var debianFolderPath = Path.Combine(BinDistDirectory, debianFolderRelativePath); Directory.CreateDirectory(debianFolderPath); newFolderName = "wasabiwallet"; var linuxWasabiWalletFolder = Tools.LinuxPathCombine(linuxUsrLocalBinFolder, newFolderName); var newFolderRelativePath = Path.Combine(debUsrLocalBinFolderRelativePath, newFolderName); newFolderPath = Path.Combine(BinDistDirectory, newFolderRelativePath); Directory.Move(publishedFolder, newFolderPath); var assetsFolder = Path.Combine(GuiProjectDirectory, "Assets"); var assetsInfo = new DirectoryInfo(assetsFolder); foreach (var file in assetsInfo.EnumerateFiles()) { var number = file.Name.Split(new string[] { "WasabiLogo", ".png" }, StringSplitOptions.RemoveEmptyEntries); if (number.Length == 1 && int.TryParse(number.First(), out int size)) { string destFolder = Path.Combine(debUsrShareIconsFolderPath, $"{size}x{size}", "apps"); Directory.CreateDirectory(destFolder); file.CopyTo(Path.Combine(destFolder, $"{ExecutableName}.png")); } } var controlFilePath = Path.Combine(debianFolderPath, "control"); // License format does not yet work, but should work in the future, it's work in progress: https://bugs.launchpad.net/ubuntu/+source/software-center/+bug/435183 var controlFileContent = $"Package: {ExecutableName}\n" + $"Priority: optional\n" + $"Section: utils\n" + $"Maintainer: nopara73 <*****@*****.**>\n" + $"Version: {VersionPrefix}\n" + $"Homepage: http://wasabiwallet.io\n" + $"Vcs-Git: git://github.com/zkSNACKs/WalletWasabi.git\n" + $"Vcs-Browser: https://github.com/zkSNACKs/WalletWasabi\n" + $"Architecture: amd64\n" + $"License: Open Source (MIT)\n" + $"Installed-Size: {installedSizeKb}\n" + $"Description: open-source, non-custodial, privacy focused Bitcoin wallet\n" + $" Built-in Tor, CoinJoin and Coin Control features.\n"; File.WriteAllText(controlFilePath, controlFileContent, Encoding.ASCII); var desktopFilePath = Path.Combine(debUsrAppFolderPath, $"{ExecutableName}.desktop"); var desktopFileContent = $"[Desktop Entry]\n" + $"Type=Application\n" + $"Name=Wasabi Wallet\n" + $"StartupWMClass=Wasabi Wallet\n" + $"GenericName=Bitcoin Wallet\n" + $"Comment=Privacy focused Bitcoin wallet.\n" + $"Icon={ExecutableName}\n" + $"Terminal=false\n" + $"Exec={ExecutableName}\n" + $"Categories=Office;Finance;\n" + $"Keywords=bitcoin;wallet;crypto;blockchain;wasabi;privacy;anon;awesome;qwe;asd;\n"; File.WriteAllText(desktopFilePath, desktopFileContent, Encoding.ASCII); var wasabiStarterScriptPath = Path.Combine(debUsrLocalBinFolderPath, $"{ExecutableName}"); var wasabiStarterScriptContent = $"#!/bin/sh\n" + $"{ linuxWasabiWalletFolder.TrimEnd('/')}/{ExecutableName} $@\n"; File.WriteAllText(wasabiStarterScriptPath, wasabiStarterScriptContent, Encoding.ASCII); string debExeLinuxPath = Tools.LinuxPathCombine(newFolderRelativePath, ExecutableName); string debDestopFileLinuxPath = Tools.LinuxPathCombine(debUsrAppFolderRelativePath, $"{ExecutableName}.desktop"); commands = new[] { "cd ~", "sudo umount /mnt/c", "sudo mount -t drvfs C: /mnt/c -o metadata", $"cd {linuxPath}", $"sudo find {Tools.LinuxPath(newFolderRelativePath)} -type f -exec chmod 644 {{}} \\;", $"sudo find {Tools.LinuxPath(newFolderRelativePath)} -type f \\( -name 'wassabee' -o -name 'hwi' -o -name 'bitcoind' \\) -exec chmod +x {{}} \\;", $"sudo chmod -R 0775 {Tools.LinuxPath(debianFolderRelativePath)}", $"sudo chmod -R 0644 {debDestopFileLinuxPath}", $"dpkg --build {Tools.LinuxPath(debFolderRelativePath)} $(pwd)" }; arguments = string.Join(" && ", commands); using (var process = Process.Start(new ProcessStartInfo { FileName = "wsl", Arguments = arguments, RedirectStandardInput = true, WorkingDirectory = BinDistDirectory })) { process.WaitForExit(); } IoHelpers.DeleteRecursivelyWithMagicDustAsync(debFolderPath).GetAwaiter().GetResult(); string oldDeb = Path.Combine(BinDistDirectory, $"{ExecutableName}_{VersionPrefix}_amd64.deb"); string newDeb = Path.Combine(BinDistDirectory, $"Wasabi-{VersionPrefix}.deb"); File.Move(oldDeb, newDeb); IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } } }
public static void Sign(ArgsProcessor argsProcessor) { if (!RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { throw new NotSupportedException("This signing method is only valid on macOS!"); } Console.WriteLine("Phase: finding the zip file on desktop which contains the compiled binaries from Windows."); string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); string removableDriveFolder = Tools.GetSingleUsbDrive(); var srcZipFileNamePattern = "WasabiToNotarize-*"; var files = Directory.GetFiles(removableDriveFolder, srcZipFileNamePattern); if (files.Length != 2) { throw new InvalidDataException($"{srcZipFileNamePattern} file missing or there are more! There must be exactly two!"); } var(appleId, password) = argsProcessor.GetAppleIdAndPassword(); while (string.IsNullOrWhiteSpace(appleId)) { Console.WriteLine("Enter appleId (email):"); appleId = Console.ReadLine(); } while (string.IsNullOrWhiteSpace(password)) { Console.WriteLine("Enter password:"******"WasabiToNotarize-2.0.0.0-arm64.zip or WasabiToNotarize-2.0.0.0.zip ". var workingDir = Path.Combine(desktopPath, "wasabiTemp"); var dmgPath = Path.Combine(workingDir, "dmg"); var unzippedPath = Path.Combine(workingDir, "unzipped"); var appName = $"{Constants.AppName}.app"; var appPath = Path.Combine(dmgPath, appName); var appContentsPath = Path.Combine(appPath, "Contents"); var appMacOsPath = Path.Combine(appContentsPath, "MacOS"); var appResPath = Path.Combine(appContentsPath, "Resources"); var appFrameworksPath = Path.Combine(appContentsPath, "Frameworks"); var infoFilePath = Path.Combine(appContentsPath, "Info.plist"); var dmgFileName = zipFile.Replace("WasabiToNotarize", "Wasabi").Replace("zip", "dmg"); var dmgFilePath = Path.Combine(workingDir, dmgFileName); var dmgUnzippedFilePath = Path.Combine(workingDir, $"Wasabi.tmp.dmg"); var appNotarizeFilePath = Path.Combine(workingDir, $"Wasabi-{versionPrefix}.zip"); var contentsPath = Path.GetFullPath(Path.Combine(Program.PackagerProjectDirectory.Replace("\\", "//"), "Content", "Osx")); var entitlementsPath = Path.Combine(contentsPath, "entitlements.plist"); var dmgContentsDir = Path.Combine(contentsPath, "Dmg"); var desktopDmgFilePath = Path.Combine(desktopPath, dmgFileName); var signArguments = $"--sign \"L233B2JQ68\" --verbose --force --options runtime --timestamp"; Console.WriteLine("Phase: creating the working directory."); if (Directory.Exists(workingDir)) { DeleteWithChmod(workingDir); } if (File.Exists(desktopDmgFilePath)) { File.Delete(desktopDmgFilePath); } Console.WriteLine("Phase: creating the app."); IoHelpers.EnsureDirectoryExists(appResPath); IoHelpers.EnsureDirectoryExists(appMacOsPath); ZipFile.ExtractToDirectory(zipPath, appMacOsPath); // Copy the binaries. IoHelpers.CopyFilesRecursively(new DirectoryInfo(Path.Combine(contentsPath, "App")), new DirectoryInfo(appPath)); Console.WriteLine("Update the plist file with current information for example with version."); var lines = File.ReadAllLines(infoFilePath); string?bundleIdentifier = null; for (int i = 0; i < lines.Length; i++) { string line = lines[i]; if (!line.TrimStart().StartsWith("<key>", StringComparison.InvariantCultureIgnoreCase)) { continue; } if (line.Contains("CFBundleShortVersionString", StringComparison.InvariantCulture) || line.Contains("CFBundleVersion", StringComparison.InvariantCulture)) { lines[i + 1] = lines[i + 1].Replace("?", $"{Version.Parse(versionPrefix).ToString(3)}"); // Apple allow only 3 version tags in plist. } else if (line.Contains("CFBundleIdentifier", StringComparison.InvariantCulture)) { bundleIdentifier = lines[i + 1].Trim().Replace("<string>", "").Replace("</string>", ""); } } if (string.IsNullOrWhiteSpace(bundleIdentifier)) { throw new InvalidDataException("Bundle identifier not found in plist file."); } File.Delete(infoFilePath); File.WriteAllLines(infoFilePath, lines); using (var process = Process.Start(new ProcessStartInfo { FileName = "chmod", Arguments = $"-R u+rwX,go+rX,go-w \"{appPath}\"", WorkingDirectory = workingDir })) { WaitProcessToFinish(process, "chmod"); } var filesToCheck = new[] { entitlementsPath }; foreach (var file in filesToCheck) { if (!File.Exists(file)) { throw new FileNotFoundException($"File missing: {file}"); } } Console.WriteLine("Signing the files in app."); IoHelpers.EnsureDirectoryExists(appResPath); IoHelpers.EnsureDirectoryExists(appMacOsPath); var executables = GetExecutables(appPath); // The main executable needs to be signed last. var filesToSignInOrder = Directory.GetFiles(appPath, "*.*", SearchOption.AllDirectories) .OrderBy(file => executables.Contains(file)) .OrderBy(file => new FileInfo(file).Name == "wassabee") .ToArray(); foreach (var file in executables) { using var process = Process.Start(new ProcessStartInfo { FileName = "chmod", Arguments = $"u+x \"{file}\"", WorkingDirectory = workingDir }); WaitProcessToFinish(process, "chmod"); } SignDirectory(filesToSignInOrder, workingDir, signArguments, entitlementsPath); Console.WriteLine("Phase: verifying the signature."); Verify(appPath); Console.WriteLine("Phase: notarize the app."); // Source: https://blog.frostwire.com/2019/08/27/apple-notarization-the-signature-of-the-binary-is-invalid-one-other-reason-not-explained-in-apple-developer-documentation/ using (var process = Process.Start(new ProcessStartInfo { FileName = "ditto", Arguments = $"-c -k --keepParent \"{appPath}\" \"{appNotarizeFilePath}\"", WorkingDirectory = workingDir })) { WaitProcessToFinish(process, "ditto"); } Notarize(appleId, password, appNotarizeFilePath, bundleIdentifier); Staple(appPath); using (var process = Process.Start(new ProcessStartInfo { FileName = "spctl", Arguments = $"-a -t exec -vv \"{appPath}\"", WorkingDirectory = workingDir, RedirectStandardError = true })) { var nonNullProcess = WaitProcessToFinish(process, "spctl"); string result = nonNullProcess.StandardError.ReadToEnd(); if (!result.Contains(": accepted")) { throw new InvalidOperationException(result); } } Console.WriteLine("Phase: creating the dmg."); if (File.Exists(dmgFilePath)) { File.Delete(dmgFilePath); } Console.WriteLine("Phase: creating dmg."); IoHelpers.CopyFilesRecursively(new DirectoryInfo(dmgContentsDir), new DirectoryInfo(dmgPath)); File.Copy(Path.Combine(contentsPath, "WasabiLogo.icns"), Path.Combine(dmgPath, ".VolumeIcon.icns"), true); var temp = Path.Combine(dmgPath, ".DS_Store.dat"); File.Move(temp, Path.Combine(dmgPath, ".DS_Store"), true); using (var process = Process.Start(new ProcessStartInfo { FileName = "ln", Arguments = "-s /Applications", WorkingDirectory = dmgPath })) { WaitProcessToFinish(process, "ln"); } var hdutilCreateArgs = string.Join( " ", new string[] { "create", $"\"{dmgUnzippedFilePath}\"", "-ov", $"-volname \"Wasabi Wallet\"", "-fs HFS+", $"-srcfolder \"{dmgPath}\"" }); using (var process = Process.Start(new ProcessStartInfo { FileName = "hdiutil", Arguments = hdutilCreateArgs, WorkingDirectory = dmgPath })) { WaitProcessToFinish(process, "hdiutil"); } var hdutilConvertArgs = string.Join( " ", new string[] { "convert", $"\"{dmgUnzippedFilePath}\"", "-format UDZO", $"-o \"{dmgFilePath}\"" }); using (var process = Process.Start(new ProcessStartInfo { FileName = "hdiutil", Arguments = hdutilConvertArgs, WorkingDirectory = dmgPath })) { WaitProcessToFinish(process, "hdiutil"); } Console.WriteLine("Phase: signing the dmg file."); SignFile($"{signArguments} --entitlements \"{entitlementsPath}\" \"{dmgFilePath}\"", dmgPath); Console.WriteLine("Phase: verifying the signature."); Verify(dmgFilePath); Console.WriteLine("Phase: notarize dmg"); Notarize(appleId, password, dmgFilePath, bundleIdentifier); Console.WriteLine("Phase: staple dmp"); Staple(dmgFilePath); using (var process = Process.Start(new ProcessStartInfo { FileName = "spctl", Arguments = $"-a -t open --context context:primary-signature -v \"{dmgFilePath}\"", WorkingDirectory = workingDir, RedirectStandardError = true })) { var nonNullProcess = WaitProcessToFinish(process, "spctl"); string result = nonNullProcess.StandardError.ReadToEnd(); if (!result.Contains(": accepted")) { throw new InvalidOperationException(result); } } File.Move(dmgFilePath, desktopDmgFilePath); DeleteWithChmod(workingDir); Console.WriteLine("Phase: finish."); var toRemovableFilePath = Path.Combine(removableDriveFolder, Path.GetFileName(desktopDmgFilePath)); File.Move(desktopDmgFilePath, toRemovableFilePath, true); if (File.Exists(zipPath)) { File.Delete(zipPath); } } }
private static void Main(string[] args) { // 0. Dump Client version (or else wrong .msi will be created) - Helpers.Constants.ClientVersion // 1. Publish with Packager. // 2. Build WIX project with Release and x64 configuration. // 3. Sign with Packager, set restore true so the password won't be kept. var doPublish = true; var doSign = false; var doRestoreThisFile = false; var pfxPassword = "******"; string pfxPath = "C:\\digicert.pfx"; string packagerProjectDirectory = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..\\..\\..\\")); string solutionDirectory = Path.GetFullPath(Path.Combine(packagerProjectDirectory, "..\\")); string guiProjectDirectory = Path.GetFullPath(Path.Combine(solutionDirectory, "WalletWasabi.Gui\\")); string libraryProjectDirectory = Path.GetFullPath(Path.Combine(solutionDirectory, "WalletWasabi\\")); string wixProjectDirectory = Path.GetFullPath(Path.Combine(solutionDirectory, "WalletWasabi.WindowsInstaller\\")); string binDistDirectory = Path.GetFullPath(Path.Combine(guiProjectDirectory, "bin\\dist")); Console.WriteLine($"{nameof(solutionDirectory)}:\t\t{solutionDirectory}"); Console.WriteLine($"{nameof(packagerProjectDirectory)}:\t{packagerProjectDirectory}"); Console.WriteLine($"{nameof(guiProjectDirectory)}:\t\t{guiProjectDirectory}"); Console.WriteLine($"{nameof(libraryProjectDirectory)}:\t\t{libraryProjectDirectory}"); Console.WriteLine($"{nameof(wixProjectDirectory)}:\t\t{wixProjectDirectory}"); Console.WriteLine($"{nameof(binDistDirectory)}:\t\t{binDistDirectory}"); string versionPrefix = Helpers.Constants.ClientVersion.ToString(); string executableName = "wassabee"; Console.WriteLine(); Console.WriteLine($"{nameof(versionPrefix)}:\t\t\t{versionPrefix}"); Console.WriteLine($"{nameof(executableName)}:\t\t\t{executableName}"); // https://docs.microsoft.com/en-us/dotnet/articles/core/rid-catalog // BOTTLENECKS: // Tor - win-32, linux-32, osx-64 // .NET Core - win-32, linux-64, osx-64 // Avalonia - win7-32, linux-64, osx-64 // We'll only support x64, if someone complains, we can come back to it. // For 32 bit Windows there needs to be a lot of WIX configuration to be done. var targets = new List <string> { "win7-x64", "linux-x64", "osx-x64" }; Console.WriteLine(); Console.Write($"{nameof(targets)}:\t\t\t"); targets.ForEach(x => { if (targets.Last() != x) { Console.Write($"{x}, "); } else { Console.Write(x); } }); Console.WriteLine(); if (doPublish) { if (Directory.Exists(binDistDirectory)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(binDistDirectory).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {binDistDirectory}"); } var psiBuild = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = guiProjectDirectory }; using (var pBuild = Process.Start(psiBuild)) { pBuild.StandardInput.WriteLine("dotnet clean --configuration Release && exit"); pBuild.WaitForExit(); } var guiBinReleaseDirectory = Path.GetFullPath(Path.Combine(guiProjectDirectory, "bin\\Release")); var libraryBinReleaseDirectory = Path.GetFullPath(Path.Combine(libraryProjectDirectory, "bin\\Release")); if (Directory.Exists(guiBinReleaseDirectory)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(guiBinReleaseDirectory).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {guiBinReleaseDirectory}"); } if (Directory.Exists(libraryBinReleaseDirectory)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(libraryBinReleaseDirectory).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {libraryBinReleaseDirectory}"); } foreach (string target in targets) { string currentBinDistDirectory; string publishedFolder = Path.Combine(binDistDirectory, target); string macWasabiAppDir = Path.Combine(publishedFolder, "Wasabi Wallet.App"); // This should be lowercase .app, but MAC will prevent people from upgrading if we change it. string macContentsDir = Path.Combine(macWasabiAppDir, "Contents"); if (target.StartsWith("osx")) { currentBinDistDirectory = Path.GetFullPath(Path.Combine(macContentsDir, "MacOS")); } else { currentBinDistDirectory = publishedFolder; } Console.WriteLine(); Console.WriteLine($"{nameof(currentBinDistDirectory)}:\t{currentBinDistDirectory}"); Console.WriteLine(); if (!Directory.Exists(currentBinDistDirectory)) { Directory.CreateDirectory(currentBinDistDirectory); Console.WriteLine($"Created {currentBinDistDirectory}"); } var psiClean = new ProcessStartInfo { FileName = "dotnet", Arguments = $"clean", WorkingDirectory = guiProjectDirectory }; using (var pClean = Process.Start(psiClean)) { pClean.WaitForExit(); } // https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish?tabs=netcore21 // -c|--configuration {Debug|Release} // Defines the build configuration. The default value is Debug. // --force // Forces all dependencies to be resolved even if the last restore was successful. Specifying this flag is the same as deleting the project.assets.json file. // -o|--output <OUTPUT_DIRECTORY> // Specifies the path for the output directory. // If not specified, it defaults to ./bin/[configuration]/[framework]/publish/ for a framework-dependent deployment or // ./bin/[configuration]/[framework]/[runtime]/publish/ for a self-contained deployment. // If the path is relative, the output directory generated is relative to the project file location, not to the current working directory. // --self-contained // Publishes the .NET Core runtime with your application so the runtime doesn't need to be installed on the target machine. // If a runtime identifier is specified, its default value is true. For more information about the different deployment types, see .NET Core application deployment. // -r|--runtime <RUNTIME_IDENTIFIER> // Publishes the application for a given runtime. This is used when creating a self-contained deployment (SCD). // For a list of Runtime Identifiers (RIDs), see the RID catalog. Default is to publish a framework-dependent deployment (FDD). // --version-suffix <VERSION_SUFFIX> // Defines the version suffix to replace the asterisk (*) in the version field of the project file. // https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-restore?tabs=netcore2x // --disable-parallel // Disables restoring multiple projects in parallel. // --no-cache // Specifies to not cache packages and HTTP requests. // https://github.com/dotnet/docs/issues/7568 // /p:Version=1.2.3.4 // "dotnet publish" supports msbuild command line options like /p:Version=1.2.3.4 var psiPublish = new ProcessStartInfo { FileName = "dotnet", Arguments = $"publish --configuration Release --force --output \"{currentBinDistDirectory}\" --self-contained true --runtime \"{target}\" /p:VersionPrefix={versionPrefix} --disable-parallel --no-cache", WorkingDirectory = guiProjectDirectory }; using (var pPublish = Process.Start(psiPublish)) { pPublish.WaitForExit(); } // Remove Tor binaries those are not relevant to the platform. var torFolder = new DirectoryInfo(Path.Combine(currentBinDistDirectory, "TorDaemons")); var toNotremove = ""; if (target.StartsWith("win")) { toNotremove = "win"; } else if (target.StartsWith("linux")) { toNotremove = "linux"; } else if (target.StartsWith("osx")) { toNotremove = "osx"; } foreach (var file in torFolder.EnumerateFiles()) { if (!file.Name.Contains("data", StringComparison.OrdinalIgnoreCase) && !file.Name.Contains(toNotremove, StringComparison.OrdinalIgnoreCase)) { File.Delete(file.FullName); } } // Rename the final exe. string oldExecutablePath; string newExecutablePath; if (target.StartsWith("win")) { oldExecutablePath = Path.Combine(currentBinDistDirectory, "WalletWasabi.Gui.exe"); newExecutablePath = Path.Combine(currentBinDistDirectory, $"{executableName}.exe"); } else // Linux & OSX { oldExecutablePath = Path.Combine(currentBinDistDirectory, "WalletWasabi.Gui"); newExecutablePath = Path.Combine(currentBinDistDirectory, executableName); } File.Move(oldExecutablePath, newExecutablePath); long installedSizeKb = DirSize(new DirectoryInfo(publishedFolder)) / 1000; if (target.StartsWith("win")) { var psiEditbin = new ProcessStartInfo { FileName = "editbin", Arguments = $"\"{newExecutablePath}\" /SUBSYSTEM:WINDOWS", WorkingDirectory = currentBinDistDirectory }; using (var pEditbin = Process.Start(psiEditbin)) { pEditbin.WaitForExit(); } var icoPath = Path.Combine(guiProjectDirectory, "Assets", "WasabiLogo.ico"); var psiRcedit = new ProcessStartInfo { FileName = "rcedit", Arguments = $"\"{newExecutablePath}\" --set-icon \"{icoPath}\" --set-file-version \"{versionPrefix}\" --set-product-version \"{versionPrefix}\" --set-version-string \"LegalCopyright\" \"MIT\" --set-version-string \"CompanyName\" \"zkSNACKs\" --set-version-string \"FileDescription\" \"Privacy focused, ZeroLink compliant Bitcoin wallet.\" --set-version-string \"ProductName\" \"Wasabi Wallet\"", WorkingDirectory = currentBinDistDirectory }; using (var pRcedit = Process.Start(psiRcedit)) { pRcedit.WaitForExit(); } } else if (target.StartsWith("osx")) { string resourcesDir = Path.Combine(macContentsDir, "Resources"); string infoFilePath = Path.Combine(macContentsDir, "Info.plist"); Directory.CreateDirectory(resourcesDir); var iconpath = Path.Combine(guiProjectDirectory, "Assets", "WasabiLogo.icns"); File.Copy(iconpath, Path.Combine(resourcesDir, "WasabiLogo.icns")); string infoContent = $@"<?xml version=""1.0"" encoding=""UTF-8""?> <!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd""> <plist version = ""1.0""> <dict> <key>LSMinimumSystemVersion</key> <string>10.12</string> <key>LSArchitecturePriority</key> <array> <string>x86_64</string> </array> <key>CFBundleIconFile</key> <string>WasabiLogo.icns</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>{versionPrefix}</string> <key>CFBundleVersion</key> <string>{versionPrefix}</string> <key>CFBundleExecutable</key> <string>{executableName}</string> <key>CFBundleName</key> <string>Wasabi Wallet</string> <key>CFBundleIdentifier</key> <string>zksnacks.wasabiwallet</string> <key>NSHighResolutionCapable</key> <true/> <key>NSAppleScriptEnabled</key> <true/> <key>LSApplicationCategoryType</key> <string>public.app-category.finance</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> </dict> </plist> "; File.WriteAllText(infoFilePath, infoContent); var psiCreateSymLink = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = publishedFolder }; using (var createSymLinkProcess = Process.Start(psiCreateSymLink)) { createSymLinkProcess.StandardInput.WriteLine($"wsl ln -s /Applications && exit"); createSymLinkProcess.WaitForExit(); } //how to generate .DS_Store file - https://github.com/zkSNACKs/WalletWasabi/pull/928/commits/e38ed672dee25f6e45a3eb16584887cc6d48c4e6 var dmgContentDir = Path.Combine(packagerProjectDirectory, "Content", "Osx"); IoHelpers.CopyFilesRecursively(new DirectoryInfo(dmgContentDir), new DirectoryInfo(publishedFolder)); var psiGenIsoImage = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; string uncompressedDmgFileName = $"Wasabi-uncompressed.dmg"; string uncompressedDmgFilePath = Path.Combine(binDistDirectory, uncompressedDmgFileName); string dmgFileName = $"Wasabi-{versionPrefix}.dmg"; using (var genIsoImageProcess = Process.Start(psiGenIsoImage)) { // http://www.nathancoulson.com/proj_cross_tools.php // -D: Do not use deep directory relocation, and instead just pack them in the way we see them // -V: Volume Label // -no-pad: Do not pad the end by 150 sectors (300kb). As it is not a cd image, not required // -apple -r: Creates a .dmg image genIsoImageProcess.StandardInput.WriteLine($"wsl genisoimage -D -V \"Wasabi Wallet\" -no-pad -apple -r -dir-mode 755 -o \"{uncompressedDmgFileName}\" \"{new DirectoryInfo(publishedFolder).Name}\" && exit"); genIsoImageProcess.WaitForExit(); } // cd ~ // git clone https://github.com/planetbeing/libdmg-hfsplus.git && cd libdmg-hfsplus // https://github.com/planetbeing/libdmg-hfsplus/issues/14 // mkdir build && cd build // sudo apt-get install zlib1g-dev // cmake .. // cd build // sudo apt-get install libssl1.0-dev // cmake .. // cd ~/libdmg-hfsplus/build/ // make var psiDmg = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; using (var dmgProcess = Process.Start(psiDmg)) { dmgProcess.StandardInput.WriteLine($"wsl ~/libdmg-hfsplus/build/dmg/./dmg dmg \"{uncompressedDmgFileName}\" \"{dmgFileName}\" && exit"); dmgProcess.WaitForExit(); } // In case compression above doesn't work: //var psiBzip = new ProcessStartInfo //{ // FileName = "cmd", // RedirectStandardInput = true, // WorkingDirectory = binDistDirectory //}; //var bzipProcess = Process.Start(psiBzip); //bzipProcess.StandardInput.WriteLine($"wsl bzip2 \"{uncompressedDmgFileName}\" && exit"); //bzipProcess.WaitForExit(); IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); File.Delete(uncompressedDmgFilePath); IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } else if (target.StartsWith("linux")) { Console.WriteLine("Create Linux .tar.gz"); if (!Directory.Exists(publishedFolder)) { throw new Exception($"{publishedFolder} doesn't exist."); } var newFolderName = $"WasabiLinux-{versionPrefix}"; var newFolderPath = Path.Combine(binDistDirectory, newFolderName); Directory.Move(publishedFolder, newFolderPath); publishedFolder = newFolderPath; var psiTar = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; using (var tarProcess = Process.Start(psiTar)) { tarProcess.StandardInput.WriteLine($"wsl tar -pczvf {newFolderName}.tar.gz {newFolderName} && exit"); tarProcess.WaitForExit(); } Console.WriteLine("Create Linux .deb"); var debFolderRelativePath = "deb"; var debFolderPath = Path.Combine(binDistDirectory, debFolderRelativePath); var linuxUsrLocalBinFolder = "/usr/local/bin/"; var debUsrLocalBinFolderRelativePath = Path.Combine(debFolderRelativePath, "usr", "local", "bin"); var debUsrLocalBinFolderPath = Path.Combine(binDistDirectory, debUsrLocalBinFolderRelativePath); Directory.CreateDirectory(debUsrLocalBinFolderPath); var debUsrAppFolderRelativePath = Path.Combine(debFolderRelativePath, "usr", "share", "applications"); var debUsrAppFolderPath = Path.Combine(binDistDirectory, debUsrAppFolderRelativePath); Directory.CreateDirectory(debUsrAppFolderPath); var debUsrShareIconsFolderRelativePath = Path.Combine(debFolderRelativePath, "usr", "share", "icons", "hicolor"); var debUsrShareIconsFolderPath = Path.Combine(binDistDirectory, debUsrShareIconsFolderRelativePath); var debianFolderRelativePath = Path.Combine(debFolderRelativePath, "DEBIAN"); var debianFolderPath = Path.Combine(binDistDirectory, debianFolderRelativePath); Directory.CreateDirectory(debianFolderPath); newFolderName = "wasabiwallet"; var linuxWasabiWalletFolder = LinuxPathCombine(linuxUsrLocalBinFolder, newFolderName); var newFolderRelativePath = Path.Combine(debUsrLocalBinFolderRelativePath, newFolderName); newFolderPath = Path.Combine(binDistDirectory, newFolderRelativePath); Directory.Move(publishedFolder, newFolderPath); var assetsFolder = Path.Combine(guiProjectDirectory, "Assets"); var assetsInfo = new DirectoryInfo(assetsFolder); foreach (var file in assetsInfo.EnumerateFiles()) { var number = file.Name.Split(new string[] { "WasabiLogo", ".png" }, StringSplitOptions.RemoveEmptyEntries); if (number.Count() == 1 && int.TryParse(number.First(), out int size)) { string destFolder = Path.Combine(debUsrShareIconsFolderPath, $"{size}x{size}", "apps"); Directory.CreateDirectory(destFolder); file.CopyTo(Path.Combine(destFolder, $"{executableName}.png")); } } var controlFilePath = Path.Combine(debianFolderPath, "control"); // License format doesn't yet work, but should work in the future, it's work in progress: https://bugs.launchpad.net/ubuntu/+source/software-center/+bug/435183 var controlFileContent = $"Package: {executableName}\n" + $"Priority: optional\n" + $"Section: utils\n" + $"Maintainer: nopara73 <*****@*****.**>\n" + $"Version: {versionPrefix}\n" + $"Homepage: http://wasabiwallet.io\n" + $"Vcs-Git: git://github.com/zkSNACKs/WalletWasabi.git\n" + $"Vcs-Browser: https://github.com/zkSNACKs/WalletWasabi\n" + $"Architecture: amd64\n" + $"License: Open Source (MIT)\n" + $"Installed-Size: {installedSizeKb}\n" + $"Description: open-source, non-custodial, privacy focused Bitcoin wallet\n" + $" Built-in Tor, CoinJoin and Coin Control features.\n"; File.WriteAllText(controlFilePath, controlFileContent, Encoding.ASCII); var desktopFilePath = Path.Combine(debUsrAppFolderPath, $"{executableName}.desktop"); var desktopFileContent = $"[Desktop Entry]\n" + $"Type=Application\n" + $"Name=Wasabi Wallet\n" + $"GenericName=Bitcoin Wallet\n" + $"Comment=Privacy focused Bitcoin wallet.\n" + $"Icon={executableName}\n" + $"Terminal=false\n" + $"Exec={executableName}\n" + $"Categories=Office;Finance;\n" + $"Keywords=bitcoin;wallet;crypto;blockchain;wasabi;privacy;anon;awesome;qwe;asd;\n"; File.WriteAllText(desktopFilePath, desktopFileContent, Encoding.ASCII); var wasabiStarterScriptPath = Path.Combine(debUsrLocalBinFolderPath, $"{executableName}"); var wasabiStarterScriptContent = $"#!/bin/sh\n" + $"{ linuxWasabiWalletFolder.TrimEnd('/')}/./{executableName}\n"; File.WriteAllText(wasabiStarterScriptPath, wasabiStarterScriptContent, Encoding.ASCII); string debExeLinuxPath = LinuxPathCombine(newFolderRelativePath, executableName); string debDestopFileLinuxPath = LinuxPathCombine(debUsrAppFolderRelativePath, $"{executableName}.desktop"); var wasabiStarterScriptLinuxPath = LinuxPathCombine(debUsrLocalBinFolderRelativePath, $"{executableName}"); var psi = new ProcessStartInfo { FileName = "wsl", Arguments = $"cd ~ && sudo umount /mnt/c && sudo mount -t drvfs C: /mnt/c -o metadata && cd /mnt/c/Users/user/Desktop/WalletWasabi/WalletWasabi.Gui/bin/dist && sudo chmod +x {debExeLinuxPath} && sudo chmod +x {wasabiStarterScriptLinuxPath} && sudo chmod -R 0644 {debDestopFileLinuxPath} && sudo chmod -R 0775 {LinuxPath(debianFolderRelativePath)} && dpkg --build {LinuxPath(debFolderRelativePath)} $(pwd)", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; using (var p = Process.Start(psi)) { p.WaitForExit(); } IoHelpers.DeleteRecursivelyWithMagicDustAsync(debFolderPath).GetAwaiter().GetResult(); string oldDeb = Path.Combine(binDistDirectory, $"{executableName}_{versionPrefix}_amd64.deb"); string newDeb = Path.Combine(binDistDirectory, $"Wasabi-{versionPrefix}.deb"); File.Move(oldDeb, newDeb); IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } } } if (doSign is true) { foreach (string target in targets) { if (target.StartsWith("win", StringComparison.OrdinalIgnoreCase)) { string publishedFolder = Path.Combine(binDistDirectory, target); Console.WriteLine("Move created .msi"); var msiPath = Path.Combine(wixProjectDirectory, @"bin\Release\Wasabi.msi"); if (!File.Exists(msiPath)) { throw new Exception(".msi doesn't exist. Expected path: Wasabi.msi."); } var msiFileName = Path.GetFileNameWithoutExtension(msiPath); var newMsiPath = Path.Combine(binDistDirectory, $"{msiFileName}-{versionPrefix}.msi"); File.Move(msiPath, newMsiPath); // Sign code with digicert. var psiSigntool = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; using (var signToolProcess = Process.Start(psiSigntool)) { signToolProcess.StandardInput.WriteLine($"signtool sign /d \"Wasabi Wallet\" /f \"{pfxPath}\" /p {pfxPassword} /t http://timestamp.digicert.com /a \"{newMsiPath}\" && exit"); signToolProcess.WaitForExit(); } IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } } Console.WriteLine("Signing final files..."); var finalFiles = Directory.GetFiles(binDistDirectory); foreach (var finalFile in finalFiles) { var psiSignProcess = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; using (var signProcess = Process.Start(psiSignProcess)) { signProcess.StandardInput.WriteLine($"gpg --armor --detach-sign {finalFile} && exit"); signProcess.WaitForExit(); } var psiRestoreHeat = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = wixProjectDirectory }; using (var restoreHeatProcess = Process.Start(psiRestoreHeat)) { restoreHeatProcess.StandardInput.WriteLine($"git checkout -- ComponentsGenerated.wxs && exit"); restoreHeatProcess.WaitForExit(); } if (doRestoreThisFile) { var psiRestoreThisFile = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = packagerProjectDirectory }; using (var restoreThisFileProcess = Process.Start(psiRestoreThisFile)) { restoreThisFileProcess.StandardInput.WriteLine($"git checkout -- Program.cs && exit"); restoreThisFileProcess.WaitForExit(); } } } IoHelpers.OpenFolderInFileExplorer(binDistDirectory); return; // No need for readkey here. } }
private static void Main(string[] args) { // 0. Dump Client version (or else wrong .msi will be created) - Helpers.Constants.ClientVersion // 1. Publish with Packager. // 2. Build WIX project with Release and x64 configuration. // 3. Sign with Packager, set restore true so the password won't be kept. var doPublish = true; var doSign = false; var doRestoreThisFile = false; var pfxPassword = "******"; string pfxPath = "C:\\digicert.pfx"; string packagerProjectDirectory = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..\\..\\..\\")); string solutionDirectory = Path.GetFullPath(Path.Combine(packagerProjectDirectory, "..\\")); string guiProjectDirectory = Path.GetFullPath(Path.Combine(solutionDirectory, "WalletWasabi.Gui\\")); string libraryProjectDirectory = Path.GetFullPath(Path.Combine(solutionDirectory, "WalletWasabi\\")); string wixProjectDirectory = Path.GetFullPath(Path.Combine(solutionDirectory, "WalletWasabi.WindowsInstaller\\")); string binDistDirectory = Path.GetFullPath(Path.Combine(guiProjectDirectory, "bin\\dist")); Console.WriteLine($"{nameof(solutionDirectory)}:\t\t{solutionDirectory}"); Console.WriteLine($"{nameof(packagerProjectDirectory)}:\t{packagerProjectDirectory}"); Console.WriteLine($"{nameof(guiProjectDirectory)}:\t\t{guiProjectDirectory}"); Console.WriteLine($"{nameof(libraryProjectDirectory)}:\t\t{libraryProjectDirectory}"); Console.WriteLine($"{nameof(wixProjectDirectory)}:\t\t{wixProjectDirectory}"); Console.WriteLine($"{nameof(binDistDirectory)}:\t\t{binDistDirectory}"); string versionPrefix = Helpers.Constants.ClientVersion.ToString(); string executableName = "wassabee"; Console.WriteLine(); Console.WriteLine($"{nameof(versionPrefix)}:\t\t\t{versionPrefix}"); Console.WriteLine($"{nameof(executableName)}:\t\t\t{executableName}"); // https://docs.microsoft.com/en-us/dotnet/articles/core/rid-catalog // BOTTLENECKS: // Tor - win-32, linux-32, osx-64 // .NET Core - win-32, linux-64, osx-64 // Avalonia - win7-32, linux-64, osx-64 // We'll only support x64, if someone complains, we can come back to it. // For 32 bit Windows there needs to be a lot of WIX configuration to be done. var targets = new List <string> { "win7-x64", "linux-x64", "osx-x64" }; Console.WriteLine(); Console.Write($"{nameof(targets)}:\t\t\t"); targets.ForEach(x => { if (targets.Last() != x) { Console.Write($"{x}, "); } else { Console.Write(x); } }); Console.WriteLine(); if (doPublish) { if (Directory.Exists(binDistDirectory)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(binDistDirectory).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {binDistDirectory}"); } var psiBuild = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = guiProjectDirectory }; using (var pBuild = Process.Start(psiBuild)) { pBuild.StandardInput.WriteLine("dotnet clean --configuration Release && exit"); pBuild.WaitForExit(); } var guiBinReleaseDirectory = Path.GetFullPath(Path.Combine(guiProjectDirectory, "bin\\Release")); var libraryBinReleaseDirectory = Path.GetFullPath(Path.Combine(libraryProjectDirectory, "bin\\Release")); if (Directory.Exists(guiBinReleaseDirectory)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(guiBinReleaseDirectory).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {guiBinReleaseDirectory}"); } if (Directory.Exists(libraryBinReleaseDirectory)) { IoHelpers.DeleteRecursivelyWithMagicDustAsync(libraryBinReleaseDirectory).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {libraryBinReleaseDirectory}"); } foreach (string target in targets) { string currentBinDistDirectory; string publishedFolder = Path.Combine(binDistDirectory, target); string macWasabiAppDir = Path.Combine(publishedFolder, "Wasabi Wallet.App"); string macContentsDir = Path.Combine(macWasabiAppDir, "Contents"); if (target.StartsWith("osx")) { currentBinDistDirectory = Path.GetFullPath(Path.Combine(macContentsDir, "MacOS")); } else { currentBinDistDirectory = publishedFolder; } Console.WriteLine(); Console.WriteLine($"{nameof(currentBinDistDirectory)}:\t{currentBinDistDirectory}"); Console.WriteLine(); if (!Directory.Exists(currentBinDistDirectory)) { Directory.CreateDirectory(currentBinDistDirectory); Console.WriteLine($"Created {currentBinDistDirectory}"); } var psiClean = new ProcessStartInfo { FileName = "dotnet", Arguments = $"clean", WorkingDirectory = guiProjectDirectory }; using (var pClean = Process.Start(psiClean)) { pClean.WaitForExit(); } // https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-publish?tabs=netcore21 // -c|--configuration {Debug|Release} // Defines the build configuration. The default value is Debug. // --force // Forces all dependencies to be resolved even if the last restore was successful. Specifying this flag is the same as deleting the project.assets.json file. // -o|--output <OUTPUT_DIRECTORY> // Specifies the path for the output directory. // If not specified, it defaults to ./bin/[configuration]/[framework]/publish/ for a framework-dependent deployment or // ./bin/[configuration]/[framework]/[runtime]/publish/ for a self-contained deployment. // If the path is relative, the output directory generated is relative to the project file location, not to the current working directory. // --self-contained // Publishes the .NET Core runtime with your application so the runtime doesn't need to be installed on the target machine. // If a runtime identifier is specified, its default value is true. For more information about the different deployment types, see .NET Core application deployment. // -r|--runtime <RUNTIME_IDENTIFIER> // Publishes the application for a given runtime. This is used when creating a self-contained deployment (SCD). // For a list of Runtime Identifiers (RIDs), see the RID catalog. Default is to publish a framework-dependent deployment (FDD). // --version-suffix <VERSION_SUFFIX> // Defines the version suffix to replace the asterisk (*) in the version field of the project file. // https://docs.microsoft.com/en-us/dotnet/core/tools/dotnet-restore?tabs=netcore2x // --disable-parallel // Disables restoring multiple projects in parallel. // --no-cache // Specifies to not cache packages and HTTP requests. // https://github.com/dotnet/docs/issues/7568 // /p:Version=1.2.3.4 // "dotnet publish" supports msbuild command line options like /p:Version=1.2.3.4 var psiPublish = new ProcessStartInfo { FileName = "dotnet", Arguments = $"publish --configuration Release --force --output \"{currentBinDistDirectory}\" --self-contained true --runtime \"{target}\" /p:VersionPrefix={versionPrefix} --disable-parallel --no-cache", WorkingDirectory = guiProjectDirectory }; using (var pPublish = Process.Start(psiPublish)) { pPublish.WaitForExit(); } // Rename the final exe. string oldExecutablePath; string newExecutablePath; if (target.StartsWith("win")) { oldExecutablePath = Path.Combine(currentBinDistDirectory, "WalletWasabi.Gui.exe"); newExecutablePath = Path.Combine(currentBinDistDirectory, $"{executableName}.exe"); } else // Linux & OSX { oldExecutablePath = Path.Combine(currentBinDistDirectory, "WalletWasabi.Gui"); newExecutablePath = Path.Combine(currentBinDistDirectory, executableName); } File.Move(oldExecutablePath, newExecutablePath); if (target.StartsWith("win")) { var psiEditbin = new ProcessStartInfo { FileName = "editbin", Arguments = $"\"{newExecutablePath}\" /SUBSYSTEM:WINDOWS", WorkingDirectory = currentBinDistDirectory }; using (var pEditbin = Process.Start(psiEditbin)) { pEditbin.WaitForExit(); } var icoPath = Path.Combine(guiProjectDirectory, "Assets", "WasabiLogo.ico"); var psiRcedit = new ProcessStartInfo { FileName = "rcedit", Arguments = $"\"{newExecutablePath}\" --set-icon \"{icoPath}\" --set-file-version \"{versionPrefix}\" --set-product-version \"{versionPrefix}\" --set-version-string \"LegalCopyright\" \"MIT\" --set-version-string \"CompanyName\" \"zkSNACKs\" --set-version-string \"FileDescription\" \"Privacy focused, ZeroLink compliant Bitcoin wallet.\" --set-version-string \"ProductName\" \"Wasabi Wallet\"", WorkingDirectory = currentBinDistDirectory }; using (var pRcedit = Process.Start(psiRcedit)) { pRcedit.WaitForExit(); } } else if (target.StartsWith("osx")) { string resourcesDir = Path.Combine(macContentsDir, "Resources"); string infoFilePath = Path.Combine(macContentsDir, "Info.plist"); Directory.CreateDirectory(resourcesDir); var iconpath = Path.Combine(guiProjectDirectory, "Assets", "WasabiLogo.icns"); File.Copy(iconpath, Path.Combine(resourcesDir, "WasabiLogo.icns")); string infoContent = $@"<?xml version=""1.0"" encoding=""UTF-8""?> <!DOCTYPE plist PUBLIC ""-//Apple//DTD PLIST 1.0//EN"" ""http://www.apple.com/DTDs/PropertyList-1.0.dtd""> <plist version = ""1.0""> <dict> <key>LSMinimumSystemVersion</key> <string>10.12</string> <key>LSArchitecturePriority</key> <array> <string>x86_64</string> </array> <key>CFBundleIconFile</key> <string>WasabiLogo.icns</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleShortVersionString</key> <string>{versionPrefix}</string> <key>CFBundleVersion</key> <string>{versionPrefix}</string> <key>CFBundleExecutable</key> <string>wassabee</string> <key>CFBundleName</key> <string>Wasabi Wallet</string> <key>CFBundleIdentifier</key> <string>zksnacks.wasabiwallet</string> <key>NSHighResolutionCapable</key> <true/> <key>NSAppleScriptEnabled</key> <true/> <key>LSApplicationCategoryType</key> <string>public.app-category.finance</string> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> </dict> </plist> "; File.WriteAllText(infoFilePath, infoContent); var psiCreateSymLink = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = publishedFolder }; using (var createSymLinkProcess = Process.Start(psiCreateSymLink)) { createSymLinkProcess.StandardInput.WriteLine($"wsl ln -s /Applications && exit"); createSymLinkProcess.WaitForExit(); } //how to generate .DS_Store file - https://github.com/zkSNACKs/WalletWasabi/pull/928/commits/e38ed672dee25f6e45a3eb16584887cc6d48c4e6 var dmgContentDir = Path.Combine(packagerProjectDirectory, "Content", "Osx"); IoHelpers.CopyFilesRecursively(new DirectoryInfo(dmgContentDir), new DirectoryInfo(publishedFolder)); var psiGenIsoImage = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; string uncompressedDmgFileName = $"Wasabi-uncompressed.dmg"; string uncompressedDmgFilePath = Path.Combine(binDistDirectory, uncompressedDmgFileName); string dmgFileName = $"Wasabi-{versionPrefix}.dmg"; using (var genIsoImageProcess = Process.Start(psiGenIsoImage)) { // http://www.nathancoulson.com/proj_cross_tools.php // -D: Do not use deep directory relocation, and instead just pack them in the way we see them // -V: Volume Label // -no-pad: Do not pad the end by 150 sectors (300kb). As it is not a cd image, not required // -apple -r: Creates a .dmg image genIsoImageProcess.StandardInput.WriteLine($"wsl genisoimage -D -V \"Wasabi Wallet\" -no-pad -apple -r -o \"{uncompressedDmgFileName}\" \"{new DirectoryInfo(publishedFolder).Name}\" && exit"); genIsoImageProcess.WaitForExit(); } // cd ~ // git clone https://github.com/planetbeing/libdmg-hfsplus.git && cd libdmg-hfsplus // https://github.com/planetbeing/libdmg-hfsplus/issues/14 // mkdir build && cd build // sudo apt-get install zlib1g-dev // cmake .. // cd build // sudo apt-get install libssl1.0-dev // cmake .. // cd ~/libdmg-hfsplus/build/ // make var psiDmg = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; using (var dmgProcess = Process.Start(psiDmg)) { dmgProcess.StandardInput.WriteLine($"wsl ~/libdmg-hfsplus/build/dmg/./dmg dmg \"{uncompressedDmgFileName}\" \"{dmgFileName}\" && exit"); dmgProcess.WaitForExit(); } // In case compression above doesn't work: //var psiBzip = new ProcessStartInfo //{ // FileName = "cmd", // RedirectStandardInput = true, // WorkingDirectory = binDistDirectory //}; //var bzipProcess = Process.Start(psiBzip); //bzipProcess.StandardInput.WriteLine($"wsl bzip2 \"{uncompressedDmgFileName}\" && exit"); //bzipProcess.WaitForExit(); IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); File.Delete(uncompressedDmgFilePath); IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } else if (target.StartsWith("linux")) { Console.WriteLine("Create Linux .tar.gz"); if (!Directory.Exists(publishedFolder)) { throw new Exception($"{publishedFolder} doesn't exist."); } var newFolderName = $"WasabiLinux-{versionPrefix}"; var newFolderPath = Path.Combine(binDistDirectory, newFolderName); Directory.Move(publishedFolder, newFolderPath); publishedFolder = newFolderPath; var psiTar = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; using (var tarProcess = Process.Start(psiTar)) { tarProcess.StandardInput.WriteLine($"wsl tar -pczvf {newFolderName}.tar.gz {newFolderName} && exit"); tarProcess.WaitForExit(); } IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } } } if (doSign is true) { foreach (string target in targets) { if (target.StartsWith("win", StringComparison.OrdinalIgnoreCase)) { string publishedFolder = Path.Combine(binDistDirectory, target); Console.WriteLine("Move created .msi"); var msiPath = Path.Combine(wixProjectDirectory, @"bin\Release\Wasabi.msi"); if (!File.Exists(msiPath)) { throw new Exception(".msi doesn't exist. Expected path: Wasabi.msi."); } var msiFileName = Path.GetFileNameWithoutExtension(msiPath); var newMsiPath = Path.Combine(binDistDirectory, $"{msiFileName}-{versionPrefix}.msi"); File.Move(msiPath, newMsiPath); // Sign code with digicert. var psiSigntool = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; using (var signToolProcess = Process.Start(psiSigntool)) { signToolProcess.StandardInput.WriteLine($"signtool sign /d \"Wasabi Wallet\" /f \"{pfxPath}\" /p {pfxPassword} /t http://timestamp.digicert.com /a \"{newMsiPath}\" && exit"); signToolProcess.WaitForExit(); } IoHelpers.DeleteRecursivelyWithMagicDustAsync(publishedFolder).GetAwaiter().GetResult(); Console.WriteLine($"Deleted {publishedFolder}"); } } Console.WriteLine("Signing final files..."); var finalFiles = Directory.GetFiles(binDistDirectory); foreach (var finalFile in finalFiles) { var psiSignProcess = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = binDistDirectory }; using (var signProcess = Process.Start(psiSignProcess)) { signProcess.StandardInput.WriteLine($"gpg --armor --detach-sign {finalFile} && exit"); signProcess.WaitForExit(); } var psiRestoreHeat = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = wixProjectDirectory }; using (var restoreHeatProcess = Process.Start(psiRestoreHeat)) { restoreHeatProcess.StandardInput.WriteLine($"git checkout -- ComponentsGenerated.wxs && exit"); restoreHeatProcess.WaitForExit(); } if (doRestoreThisFile) { var psiRestoreThisFile = new ProcessStartInfo { FileName = "cmd", RedirectStandardInput = true, WorkingDirectory = packagerProjectDirectory }; using (var restoreThisFileProcess = Process.Start(psiRestoreThisFile)) { restoreThisFileProcess.StandardInput.WriteLine($"git checkout -- Program.cs && exit"); restoreThisFileProcess.WaitForExit(); } } } IoHelpers.OpenFolderInFileExplorer(binDistDirectory); return; // No need for readkey here. } }