using System; using System.Collections.Generic; using System.IO; using System.IO.Compression; using System.Linq; using Cake.Core.Diagnostics; using Cake.Core.IO; using Cake.Frosting; using Cake.Powershell; using Path = System.IO.Path; namespace Build.Tasks; [TaskName("CopyOutput")] public class CopyOutputTask : FrostingTask { public override void Run(BuildContext context) { var powershellModuleName = "PowershellModule"; // TODO: probably don't create full file locations when I can pass the output dir in and have the script // make the path var scriptParams = new { PowershellModuleFileLocation = $"{context.PowershellModuleOutputDir}/{powershellModuleName}.psd1", Guid = Guid.Parse("5cdf4635-edb0-428c-8d9b-92d0bcd47443"), Author = "Me", NestedModules = new[] { $"{powershellModuleName}.dll" }, RootModule = $"{powershellModuleName}.psm1", CmdletsToExport = GetExportedCmdlets(), ManifestFileLocation = $"{context.PowershellModuleOutputDir}/{powershellModuleName}.psm1", OutputLocation = context.PowershellModuleOutputDir, }; var psSettings = new PowershellSettings() { Arguments = new ProcessArgumentBuilder(), }; // This feels a bit ugly/redundant seeing as I've defined the scriptParams above, but I'm leaving it as is // until I want to come back and clean this up properly psSettings.Arguments.Append("powershellModuleFileLocation", ToPowershellSafeString(scriptParams.PowershellModuleFileLocation)); psSettings.Arguments.Append("guid", ToPowershellSafeString(scriptParams.Guid.ToString())); psSettings.Arguments.Append("author", ToPowershellSafeString(scriptParams.Author)); psSettings.Arguments.Append("nestedModules", $"@({string.Join(",", scriptParams.NestedModules.Select(ToPowershellSafeString))})"); psSettings.Arguments.Append("rootModule", ToPowershellSafeString(scriptParams.RootModule)); psSettings.Arguments.Append("cmdletsToExport", $"@({string.Join(",", scriptParams.CmdletsToExport.Select(ToPowershellSafeString))})"); psSettings.Arguments.Append("manifestFileLocation", ToPowershellSafeString(scriptParams.ManifestFileLocation)); psSettings.Arguments.Append("outputDir", ToPowershellSafeString(scriptParams.OutputLocation)); context.StartPowershellFile(context.CreateModuleManifestScript, psSettings); context.Log.Information($"Module output to: {context.PowershellModuleOutputDir.Path.MakeAbsolute(context.Environment)}"); BundleMinimalFiles(context); base.Run(context); } private static string ToPowershellSafeString(string unescapedString) => $"'{unescapedString}'"; private static List GetExportedCmdlets() { return ["Get-Calendar", .. GetGitRepoRegistrationVerbs()]; } private static List GetGitRepoRegistrationVerbs() { // TODO: I should make some reference file for these verbs and name but that'd pull in powershell dependencies // to the build and I'd like to avoid that. // This isn't great but commands are unlikely to change that frequently string[] verbs = ["Get", "New", "Remove", "Show"]; var commandName = "GitRepoRegistration"; return [.. verbs.Select(x => $"{x}-{commandName}")]; } private static void BundleMinimalFiles(BuildContext context) { // We're effectively replicating PostBuild.ps1 with all of this as Remove-Item at the end of it does not remove files the same way. // TODO: See if that's something I can fix var powershellModuleBuildLocation = context.PowershellModuleOutputDir.Path.MakeAbsolute(context.Environment).FullPath; // PowershellModule is here so that the script that adds to the users $Env:PSModulePath correctly resolves when using // Import-Module "PowershellModule" // TODO: Generate the script to add to the PSModulePath in the bundleOutputLocation directory // Top level bundle output location var bundleRootLocation = Directory.CreateDirectory(Path.Combine(powershellModuleBuildLocation, "bundle")).FullName; // Module folder location that will be zipped up and will contain any setup scripts/readme etc var moduleFolderLocation = Directory.CreateDirectory(Path.Combine(bundleRootLocation, "NulahModule")).FullName; // Location to put all the module dlls and other files var powershelModuleOutputLocation = Directory.CreateDirectory(Path.Combine(moduleFolderLocation, "PowershellModule")).FullName; context.Log.Information($"Bundle location: {bundleRootLocation}"); context.Log.Information($"Built file location: {powershellModuleBuildLocation}"); // PostBuild.ps1 for PowershellModule should have already been run at this point - it won't remove files, but it will copy the // correct e_sqlite3.dll that we need. // TODO: Investigate this later as I'm not entirely happy with this now // // Get the location for the specific version of e_sqlite3.dll we need - PowerShell binary modules resolve dependent dlls // // from the executing assembly directory first, and the built output of the module includes a lot of other dlls that // // are already available with the dotnet runtime so there's no need for us to include them. // // I mean, we could, but we'd also be terrible software engineers if we couldn't do something as basic as reducing files we need. // // TODO: Account for other runtimes such as linux based ones where the dotnet runtime might _not_ provide these files for free. // // It'd require looking into how the module loads for PowerShell on those operating systems, and I might never // // get around to doing that because I use windows (currently). // // TODO: Update this later to handle other runtimes, for now we hardcode to win-x64 because it's what I use // var nativeSqliteDllLocation = System.IO.Path.Combine(powershellModuleBuildLocation, "runtimes", "win-x64", "native", "e_sqlite3.dll"); // // if (!File.Exists(nativeSqliteDllLocation)) // { // context.Log.Error($"BUNDLE FAILED: Unable to locate ${nativeSqliteDllLocation}"); // return; // } // // context.Log.Information($"Copying '${nativeSqliteDllLocation}' to '${bundleOutputLocation}'"); // // File.Copy(nativeSqliteDllLocation, System.IO.Path.Combine(bundleOutputLocation, System.IO.Path.GetFileName(nativeSqliteDllLocation))); var moduleCoreFiles = Directory.GetFiles(powershellModuleBuildLocation, "ModuleCore*", searchOption: SearchOption.TopDirectoryOnly); var powershellModuleFiles = Directory.GetFiles(powershellModuleBuildLocation, "PowershellModule*", searchOption: SearchOption.TopDirectoryOnly); var sqliteFiles = Directory.GetFiles(powershellModuleBuildLocation, "*SQLite*", searchOption: SearchOption.TopDirectoryOnly); string[] allFiles = [.. moduleCoreFiles, .. powershellModuleFiles, .. sqliteFiles]; context.Log.Information($"Copying {allFiles.Length} files to the module folder {powershelModuleOutputLocation}"); foreach (var file in allFiles) { context.Log.Information($"Copying {Path.GetFileName(file)}"); File.Copy(file, Path.Combine(powershelModuleOutputLocation, Path.GetFileName(file))); } context.Log.Information($"Bundle files copied"); GenerateModuleImportScript(moduleFolderLocation, context); CreateBundleZip(bundleRootLocation, moduleFolderLocation, context); } private static void GenerateModuleImportScript(string bundleLocation, BuildContext context) { context.Log.Information($"Generating module import script"); using var scriptFile = File.Create(Path.Combine(bundleLocation, "NulahPowershell.ps1")); using var fileWriter = new StreamWriter(scriptFile); // TODO: Document the set up function // TODO: Create a ticket for fleshing out the set up function (or just do that work later) fileWriter.Write( """ # To use, first copy all files to the same location as your $profile under ./NulahModule, then open your powershell profile located at $profile and add the following: # Script setup style: Using the setup script to import as needed (this method will also set custom prompts and other alias functions) # . "$PSScriptRoot/NulahModule/NulahPowershell.ps1" # SetupNulahPowershell # Just the module: Use the following to just import just the powershell module # Import-Module -Name "$PSScriptRoot/NulahModule/PowershellModule" function SetupNulahPowershell { # only add to our module path once # The intent for this is that a user will have copied this bundle folder alongside their $profile # location, eg, they'll have a folder called NulahModule that will contain everything within the bundle zip $testBundleDirectory = $PSScriptRoot $bundleInPath = ($Env:PSModulePath -split ';').TrimEnd('\') -contains $testBundleDirectory; if ($false -eq $bundleInPath) { $env:PSModulePath = @( $env:PSModulePath $testBundleDirectory ) -Join [System.IO.Path]::PathSeparator } Import-Module "PowershellModule" } """ ); context.Log.Information($"Module import script created"); } private static void CreateBundleZip(string bundleLocation, string moduleFolderLocation, BuildContext context) { var bundleZipFileLocaiton = Path.Combine(bundleLocation, "NulahModule.zip"); ZipFile.CreateFromDirectory(moduleFolderLocation, bundleZipFileLocaiton, CompressionLevel.Fastest, true); } }