/// <summary> /// Generate a set of preprocessor variable definitions using the metadata. /// </summary> /// <returns>An enumerable containing package metadata converted to WiX preprocessor definitions.</returns> private IEnumerable <string> GetPreprocessorDefinitions(NugetPackage package, string platform) { yield return($@"PackageId={package.Id}"); yield return($@"PackageVersion={package.Version}"); yield return($@"ProductVersion={package.ProductVersion}"); yield return($@"ProductCode={Utils.CreateUuid(ProductCodeNamespaceUuid, package.Identity.ToString() + $"{platform}"):B}"); yield return($@"UpgradeCode={Utils.CreateUuid(UpgradeCodeNamespaceUuid, package.Identity.ToString() + $"{platform}"):B}"); }
private string GeneratePackageProject(string msiPath, string msiJsonPath, string platform, NugetPackage nupkg) { string msiPackageProject = Path.Combine(MsiPackageDirectory, platform, nupkg.Id, "msi.csproj"); string msiPackageProjectDir = Path.GetDirectoryName(msiPackageProject); Log?.LogMessage($"Generating package project: '{msiPackageProject}'"); if (Directory.Exists(msiPackageProjectDir)) { Directory.Delete(msiPackageProjectDir, recursive: true); } Directory.CreateDirectory(msiPackageProjectDir); string iconFileName = "Icon.png"; string licenseFileName = "LICENSE.TXT"; EmbeddedTemplates.Extract(iconFileName, msiPackageProjectDir); EmbeddedTemplates.Extract(licenseFileName, msiPackageProjectDir); XmlWriterSettings settings = new XmlWriterSettings { Indent = true, IndentChars = " ", }; XmlWriter writer = XmlWriter.Create(msiPackageProject, settings); writer.WriteStartElement("Project"); writer.WriteAttributeString("Sdk", "Microsoft.NET.Sdk"); writer.WriteStartElement("PropertyGroup"); writer.WriteElementString("TargetFramework", "net5.0"); writer.WriteElementString("GeneratePackageOnBuild", "true"); writer.WriteElementString("IncludeBuildOutput", "false"); writer.WriteElementString("IsPackable", "true"); writer.WriteElementString("PackageType", "DotnetPlatform"); writer.WriteElementString("SuppressDependenciesWhenPacking", "true"); writer.WriteElementString("NoWarn", "$(NoWarn);NU5128"); writer.WriteElementString("PackageId", $"{nupkg.Id}.Msi.{platform}"); writer.WriteElementString("PackageVersion", $"{nupkg.Version}"); writer.WriteElementString("Description", nupkg.Description); if (!string.IsNullOrWhiteSpace(nupkg.Authors)) { writer.WriteElementString("Authors", nupkg.Authors); } if (!string.IsNullOrWhiteSpace(nupkg.Copyright)) { writer.WriteElementString("Copyright", nupkg.Copyright); } if (!string.IsNullOrWhiteSpace(nupkg.ProjectUrl)) { writer.WriteElementString("PackageProjectUrl", nupkg.ProjectUrl); } writer.WriteElementString("PackageLicenseExpression", "MIT"); writer.WriteEndElement(); writer.WriteStartElement("ItemGroup"); WriteItem(writer, "None", msiPath, @"\data"); WriteItem(writer, "None", msiJsonPath, @"\data\msi.json"); WriteItem(writer, "None", licenseFileName, @"\"); writer.WriteEndElement(); // ItemGroup writer.WriteRaw(@" <Target Name=""AddPackageIcon"" BeforeTargets=""$(GenerateNuspecDependsOn)"" Condition=""'$(PackageIcon)' == ''""> <PropertyGroup> <PackageIcon>Icon.png</PackageIcon> </PropertyGroup> <ItemGroup Condition=""'$(IsPackable)' == 'true'""> <None Include=""$(PackageIcon)"" Pack=""true"" PackagePath=""$(PackageIcon)"" Visible=""false"" /> </ItemGroup> </Target> "); writer.WriteEndElement(); // Project writer.Flush(); writer.Close(); return(msiPackageProject); }
/// <summary> /// Generate a set of MSIs for the specified platforms using the specified NuGet package. /// </summary> /// <param name="sourcePackage">The NuGet package to convert into an MSI.</param> /// <param name="outputPath">The output path of the generated MSI.</param> /// <param name="platforms"></param> protected IEnumerable <ITaskItem> Generate(string sourcePackage, string swixPackageId, string outputPath, WorkloadPackKind kind, params string[] platforms) { NugetPackage nupkg = null; lock (extractNuGetLock) { nupkg = new(sourcePackage, Log); } List <TaskItem> msis = new(); // MSI ProductName defaults to the package title and fallback to the package ID with a warning. string productName = nupkg.Title; if (string.IsNullOrWhiteSpace(nupkg.Title)) { Log?.LogMessage(MessageImportance.High, $"'{sourcePackage}' should have a non-empty title. The MSI ProductName will be set to the package ID instead."); productName = nupkg.Id; } // Extract once, but harvest multiple times because some generated attributes are platform dependent. string packageContentsDirectory = Path.Combine(PackageDirectory, $"{nupkg.Identity}"); IEnumerable <string> exclusions = GetExlusionPatterns(); string installDir = GetInstallDir(kind); string packKind = kind.ToString().ToLowerInvariant(); if ((kind != WorkloadPackKind.Library) && (kind != WorkloadPackKind.Template)) { Log.LogMessage(MessageImportance.Low, $"Extracting '{sourcePackage}' to '{packageContentsDirectory}'"); lock (extractNuGetLock) { nupkg.Extract(packageContentsDirectory, exclusions); } } else { // Library and template packs are not extracted. We want to harvest the nupkg itself, // instead of the contents. The package is still copied to a separate folder for harvesting // to avoid accidentally pulling in additional files and directories. Log.LogMessage(MessageImportance.Low, $"Copying '{sourcePackage}' to '{packageContentsDirectory}'"); if (Directory.Exists(packageContentsDirectory)) { Directory.Delete(packageContentsDirectory, recursive: true); } Directory.CreateDirectory(packageContentsDirectory); File.Copy(sourcePackage, Path.Combine(packageContentsDirectory, Path.GetFileName(sourcePackage))); } System.Threading.Tasks.Parallel.ForEach(platforms, platform => { // Extract the MSI template and add it to the list of source files. List <string> sourceFiles = new(); string msiSourcePath = Path.Combine(MsiDirectory, $"{nupkg.Id}", $"{nupkg.Version}", platform); sourceFiles.Add(EmbeddedTemplates.Extract("DependencyProvider.wxs", msiSourcePath)); sourceFiles.Add(EmbeddedTemplates.Extract("Directories.wxs", msiSourcePath)); sourceFiles.Add(EmbeddedTemplates.Extract("dotnethome_x64.wxs", msiSourcePath)); sourceFiles.Add(EmbeddedTemplates.Extract("Product.wxs", msiSourcePath)); sourceFiles.Add(EmbeddedTemplates.Extract("Registry.wxs", msiSourcePath)); string EulaRtfPath = Path.Combine(msiSourcePath, "eula.rtf"); File.WriteAllText(EulaRtfPath, Eula.Replace("__LICENSE_URL__", nupkg.LicenseUrl)); EmbeddedTemplates.Extract("Variables.wxi", msiSourcePath); // Harvest the package contents and add it to the source files we need to compile. string packageContentWxs = Path.Combine(msiSourcePath, "PackageContent.wxs"); sourceFiles.Add(packageContentWxs); string directoryReference = (kind == WorkloadPackKind.Library) || (kind == WorkloadPackKind.Template) ? "InstallDir" : PackageContentDirectoryReference; HarvestToolTask heat = new(BuildEngine, WixToolsetPath) { ComponentGroupName = PackageContentComponentGroupName, DirectoryReference = directoryReference, OutputFile = packageContentWxs, Platform = platform, SourceDirectory = packageContentsDirectory }; lock (extractNuGetLock) { if (!heat.Execute()) { throw new Exception($"Failed to harvest package contents."); } } // Compile the MSI sources string candleIntermediateOutputPath = Path.Combine(IntermediateBaseOutputPath, "wixobj", $"{nupkg.Id}", $"{nupkg.Version}", platform); CompileToolTask candle = new(BuildEngine, WixToolsetPath) { // Candle expects the output path to end with a single '\' OutputPath = candleIntermediateOutputPath.TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar, SourceFiles = sourceFiles, Arch = platform }; // Configure preprocessor definitions. string manufacturer = "Microsoft Corporation"; if (!string.IsNullOrWhiteSpace(nupkg.Authors) && (nupkg.Authors.IndexOf("Microsoft", StringComparison.OrdinalIgnoreCase) < 0)) { manufacturer = nupkg.Authors; } Log.LogMessage(MessageImportance.Low, $"Setting Manufacturer to '{manufacturer}'"); candle.PreprocessorDefinitions.AddRange(GetPreprocessorDefinitions(nupkg, platform)); candle.PreprocessorDefinitions.Add($@"InstallDir={installDir}"); candle.PreprocessorDefinitions.Add($@"ProductName={productName}"); candle.PreprocessorDefinitions.Add($@"Platform={platform}"); candle.PreprocessorDefinitions.Add($@"SourceDir={packageContentsDirectory}"); candle.PreprocessorDefinitions.Add($@"Manufacturer={manufacturer}"); candle.PreprocessorDefinitions.Add($@"EulaRtf={EulaRtfPath}"); candle.PreprocessorDefinitions.Add($@"PackKind={packKind}"); // Compiler extension to process dependency provider authoring for package reference counting. candle.Extensions.Add("WixDependencyExtension"); if (!candle.Execute()) { throw new Exception($"Failed to compile MSI."); } // Link the MSI. The generated filename contains a the semantic version (excluding build metadata) and platform. // If the source package already contains a platform, e.g. an aliased package that has a RID, then we don't add // the platform again. string shortPackageName = Path.GetFileNameWithoutExtension(sourcePackage).Replace(ShortNames); string outputFile = sourcePackage.Contains(platform) ? Path.Combine(OutputPath, shortPackageName + ".msi") : Path.Combine(OutputPath, shortPackageName + $"-{platform}.msi"); LinkToolTask light = new(BuildEngine, WixToolsetPath) { OutputFile = Path.Combine(OutputPath, outputFile), SourceFiles = Directory.EnumerateFiles(candleIntermediateOutputPath, "*.wixobj"), SuppressIces = this.SuppressIces }; // Add WiX extensions light.Extensions.Add("WixDependencyExtension"); light.Extensions.Add("WixUIExtension"); if (!light.Execute()) { throw new Exception($"Failed to link MSI."); } // Generate metadata used for CLI based installations. string msiPath = light.OutputFile; MsiProperties msiProps = new MsiProperties { InstallSize = MsiUtils.GetInstallSize(msiPath), Language = Convert.ToInt32(MsiUtils.GetProperty(msiPath, "ProductLanguage")), Payload = Path.GetFileName(msiPath), ProductCode = MsiUtils.GetProperty(msiPath, "ProductCode"), ProductVersion = MsiUtils.GetProperty(msiPath, "ProductVersion"), ProviderKeyName = $"{nupkg.Id},{nupkg.Version},{platform}", UpgradeCode = MsiUtils.GetProperty(msiPath, "UpgradeCode"), RelatedProducts = MsiUtils.GetRelatedProducts(msiPath) }; string msiJsonPath = Path.Combine(Path.GetDirectoryName(msiPath), Path.GetFileNameWithoutExtension(msiPath) + ".json"); File.WriteAllText(msiJsonPath, JsonSerializer.Serialize <MsiProperties>(msiProps)); TaskItem msi = new(light.OutputFile); msi.SetMetadata(Metadata.Platform, platform); msi.SetMetadata(Metadata.Version, nupkg.ProductVersion); msi.SetMetadata(Metadata.JsonProperties, msiJsonPath); msi.SetMetadata(Metadata.WixObj, candleIntermediateOutputPath); if (GenerateSwixAuthoring && IsSupportedByVisualStudio(platform)) { string swixProject = GenerateSwixPackageAuthoring(light.OutputFile, !string.IsNullOrWhiteSpace(swixPackageId) ? swixPackageId : $"{nupkg.Id.Replace(ShortNames)}.{nupkg.Version}", platform); if (!string.IsNullOrWhiteSpace(swixProject)) { msi.SetMetadata(Metadata.SwixProject, swixProject); } } // Generate a .csproj to build a NuGet payload package to carry the MSI and JSON manifest msi.SetMetadata(Metadata.PackageProject, GeneratePackageProject(msi.ItemSpec, msiJsonPath, platform, nupkg)); msis.Add(msi); }); return(msis); }