diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index 344b69631..09f28ff74 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -545,6 +545,10 @@ private List InstallPackage( } moduleManifestVersion = parsedMetadataHashtable["ModuleVersion"] as string; + pkg.CompanyName = parsedMetadataHashtable.ContainsKey("CompanyName") ? parsedMetadataHashtable["CompanyName"] as string : String.Empty; + pkg.Copyright = parsedMetadataHashtable.ContainsKey("Copyright") ? parsedMetadataHashtable["Copyright"] as string : String.Empty; + pkg.ReleaseNotes = parsedMetadataHashtable.ContainsKey("ReleaseNotes") ? parsedMetadataHashtable["ReleaseNotes"] as string : String.Empty; + pkg.RepositorySourceLocation = repoUri; // Accept License verification if (!_savePkg && !CallAcceptLicense(pkg, moduleManifest, tempInstallPath, newVersion)) @@ -558,6 +562,32 @@ private List InstallPackage( continue; } } + else + { + // is script + if (!PSScriptFileInfo.TryTestPSScriptFile( + scriptFileInfoPath: scriptPath, + parsedScript: out PSScriptFileInfo scriptToInstall, + out ErrorRecord[] errors, + out string[] _ + )) + { + foreach (ErrorRecord error in errors) + { + _cmdletPassedIn.WriteError(error); + } + + continue; + } + + Hashtable parsedMetadataHashtable = scriptToInstall.ToHashtable(); + + moduleManifestVersion = parsedMetadataHashtable["ModuleVersion"] as string; + pkg.CompanyName = parsedMetadataHashtable.ContainsKey("CompanyName") ? parsedMetadataHashtable["CompanyName"] as string : String.Empty; + pkg.Copyright = parsedMetadataHashtable.ContainsKey("Copyright") ? parsedMetadataHashtable["Copyright"] as string : String.Empty; + pkg.ReleaseNotes = parsedMetadataHashtable.ContainsKey("ReleaseNotes") ? parsedMetadataHashtable["ReleaseNotes"] as string : String.Empty; + pkg.RepositorySourceLocation = repoUri; + } // Delete the extra nupkg related files that are not needed and not part of the module/script DeleteExtraneousFiles(pkgIdentity, tempDirNameVersion); diff --git a/src/code/PSResourceInfo.cs b/src/code/PSResourceInfo.cs index 6d1523cc4..ca6057002 100644 --- a/src/code/PSResourceInfo.cs +++ b/src/code/PSResourceInfo.cs @@ -234,8 +234,8 @@ public sealed class PSResourceInfo public Dictionary AdditionalMetadata { get; } public string Author { get; } - public string CompanyName { get; } - public string Copyright { get; } + public string CompanyName { get; internal set; } + public string Copyright { get; internal set; } public Dependency[] Dependencies { get; } public string Description { get; } public Uri IconUri { get; } @@ -250,9 +250,9 @@ public sealed class PSResourceInfo public string Prerelease { get; } public Uri ProjectUri { get; } public DateTime? PublishedDate { get; } - public string ReleaseNotes { get; } + public string ReleaseNotes { get; internal set; } public string Repository { get; } - public string RepositorySourceLocation { get; } + public string RepositorySourceLocation { get; internal set; } public string[] Tags { get; } public ResourceType Type { get; } public DateTime? UpdatedDate { get; } diff --git a/src/code/PSScriptFileInfo.cs b/src/code/PSScriptFileInfo.cs index dfeee2121..9e568554e 100644 --- a/src/code/PSScriptFileInfo.cs +++ b/src/code/PSScriptFileInfo.cs @@ -1,3 +1,4 @@ +using System.Collections; // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. @@ -489,6 +490,31 @@ out ErrorRecord[] errors return fileContentsSuccessfullyCreated; } + + internal Hashtable ToHashtable() + { + Hashtable scriptHashtable = new Hashtable(StringComparer.OrdinalIgnoreCase); + + Hashtable metadataObjectHashtable = ScriptMetadataComment.ToHashtable(); + foreach(string key in metadataObjectHashtable.Keys) + { + if (!scriptHashtable.ContainsKey(key)) + { + // shouldn't have duplicate keys, but just for unexpected error handling + scriptHashtable.Add(key, metadataObjectHashtable[key]); + } + } + + scriptHashtable.Add(nameof(ScriptHelpComment.Description), ScriptHelpComment.Description); + + if (ScriptRequiresComment.RequiredModules.Length != 0) + { + scriptHashtable.Add(nameof(ScriptRequiresComment.RequiredModules), ScriptRequiresComment.RequiredModules); + } + + return scriptHashtable; + } + #endregion } } diff --git a/src/code/PSScriptMetadata.cs b/src/code/PSScriptMetadata.cs index 8ce8d0360..909d76210 100644 --- a/src/code/PSScriptMetadata.cs +++ b/src/code/PSScriptMetadata.cs @@ -544,6 +544,29 @@ internal bool UpdateContent( return true; } + + internal Hashtable ToHashtable() + { + // Constructor would be called first, which handles empty null values for required properties. + Hashtable metadataHashtable = new Hashtable(StringComparer.OrdinalIgnoreCase); + metadataHashtable.Add(nameof(Version), Version); + metadataHashtable.Add(nameof(Guid), Guid); + metadataHashtable.Add(nameof(Author), Author); + metadataHashtable.Add(nameof(CompanyName), CompanyName); + metadataHashtable.Add(nameof(Copyright), Copyright); + metadataHashtable.Add(nameof(Tags), Tags); + metadataHashtable.Add(nameof(LicenseUri), LicenseUri); + metadataHashtable.Add(nameof(ProjectUri), ProjectUri); + metadataHashtable.Add(nameof(IconUri), IconUri); + metadataHashtable.Add(nameof(ExternalModuleDependencies), ExternalModuleDependencies); + metadataHashtable.Add(nameof(RequiredScripts), RequiredScripts); + metadataHashtable.Add(nameof(ExternalScriptDependencies), ExternalScriptDependencies); + metadataHashtable.Add(nameof(ReleaseNotes), ReleaseNotes); + metadataHashtable.Add(nameof(PrivateData), PrivateData); + + return metadataHashtable; + } + #endregion } } diff --git a/src/code/PublishPSResource.cs b/src/code/PublishPSResource.cs index 31c3d85f9..545c354ff 100644 --- a/src/code/PublishPSResource.cs +++ b/src/code/PublishPSResource.cs @@ -219,29 +219,31 @@ protected override void EndProcessing() return; } - Hashtable parsedMetadata; + Hashtable parsedMetadata = new Hashtable(StringComparer.OrdinalIgnoreCase); if (resourceType == ResourceType.Script) { - // Check that script metadata is valid - if (!TryParseScriptMetadata( - out parsedMetadata, - pathToScriptFileToPublish, - out ErrorRecord[] errors)) + if (!PSScriptFileInfo.TryTestPSScriptFile( + scriptFileInfoPath: pathToScriptFileToPublish, + parsedScript: out PSScriptFileInfo scriptToPublish, + out ErrorRecord[] errors, + out string[] _ + )) { - foreach (ErrorRecord err in errors) + foreach (ErrorRecord error in errors) { - WriteError(err); + WriteError(error); } return; } + parsedMetadata = scriptToPublish.ToHashtable(); + _pkgName = System.IO.Path.GetFileNameWithoutExtension(pathToScriptFileToPublish); } else { // parsedMetadata needs to be initialized for modules, will later be passed in to create nuspec - parsedMetadata = new Hashtable(); if (!string.IsNullOrEmpty(pathToModuleManifestToPublish)) { _pkgName = System.IO.Path.GetFileNameWithoutExtension(pathToModuleManifestToPublish); @@ -746,183 +748,6 @@ private Hashtable ParseRequiredModules(Hashtable parsedMetadataHash) return dependenciesHash; } - private bool TryParseScriptMetadata( - out Hashtable parsedMetadata, - string filePath, - out ErrorRecord[] errors) - { - parsedMetadata = new Hashtable(); - List parseMetadataErrors = new List(); - - // a valid example script will have this format: - /* <#PSScriptInfo - .VERSION 1.6 - .GUID abf490023 - 9128 - 4323 - sdf9a - jf209888ajkl - .AUTHOR Jane Doe - .COMPANYNAME Microsoft - .COPYRIGHT - .TAGS Windows MacOS - #> - - <# - - .SYNOPSIS - Synopsis description here - .DESCRIPTION - Description here - .PARAMETER Name - .EXAMPLE - Example cmdlet here - - #> - */ - - // Parse the script file - var ast = Parser.ParseFile( - filePath, - out System.Management.Automation.Language.Token[] tokens, - out ParseError[] parserErrors); - - if (parserErrors.Length > 0) - { - foreach (ParseError err in parserErrors) - { - // we ignore WorkFlowNotSupportedInPowerShellCore errors, as this is common in scripts currently on PSGallery - if (!String.Equals(err.ErrorId, "WorkflowNotSupportedInPowerShellCore", StringComparison.OrdinalIgnoreCase)) - { - var message = String.Format("Could not parse '{0}' as a PowerShell script file due to {1}.", filePath, err.Message); - var ex = new ArgumentException(message); - var psScriptFileParseError = new ErrorRecord(ex, err.ErrorId, ErrorCategory.ParserError, null); - parseMetadataErrors.Add(psScriptFileParseError); - } - } - - errors = parseMetadataErrors.ToArray(); - return false; - } - - if (ast == null) - { - var astNullMessage = String.Format(".ps1 file was parsed but AST was null"); - var astNullEx = new ArgumentException(astNullMessage); - var astCouldNotBeCreatedError = new ErrorRecord(astNullEx, "ASTCouldNotBeCreated", ErrorCategory.ParserError, null); - - parseMetadataErrors.Add(astCouldNotBeCreatedError); - errors = parseMetadataErrors.ToArray(); - return false; - - } - - // Get the block/group comment beginning with <#PSScriptInfo - List commentTokens = tokens.Where(a => String.Equals(a.Kind.ToString(), "Comment", StringComparison.OrdinalIgnoreCase)).ToList(); - string commentPattern = PSScriptInfoCommentString; - Regex rg = new Regex(commentPattern); - List psScriptInfoCommentTokens = commentTokens.Where(a => rg.IsMatch(a.Extent.Text)).ToList(); - - if (psScriptInfoCommentTokens.Count() == 0 || psScriptInfoCommentTokens[0] == null) - { - var message = String.Format("PSScriptInfo comment was missing or could not be parsed"); - var ex = new ArgumentException(message); - var psCommentMissingError = new ErrorRecord(ex, "psScriptInfoCommentMissingError", ErrorCategory.ParserError, null); - parseMetadataErrors.Add(psCommentMissingError); - errors = parseMetadataErrors.ToArray(); - return false; - } - - string[] commentLines = Regex.Split(psScriptInfoCommentTokens[0].Text, "[\r\n]").Where(x => !String.IsNullOrEmpty(x)).ToArray(); - string keyName = String.Empty; - string value = String.Empty; - - /** - If comment line count is not more than two, it doesn't have the any metadata property - comment block would look like: - <#PSScriptInfo - #> - */ - - if (commentLines.Count() > 2) - { - for (int i = 1; i < commentLines.Count(); i++) - { - string line = commentLines[i]; - if (String.IsNullOrEmpty(line)) - { - continue; - } - - // A line is starting with . conveys a new metadata property - if (line.Trim().StartsWith(".")) - { - string[] parts = line.Trim().TrimStart('.').Split(); - keyName = parts[0].ToLower(); - value = parts.Count() > 1 ? String.Join(" ", parts.Skip(1)) : String.Empty; - parsedMetadata.Add(keyName, value); - } - } - } - - // get .DESCRIPTION comment - CommentHelpInfo scriptCommentInfo = ast.GetHelpContent(); - if (scriptCommentInfo == null) - { - var message = String.Format("PSScript file is missing the required Description comment block in the script contents."); - var ex = new ArgumentException(message); - var psScriptMissingHelpContentCommentBlockError = new ErrorRecord(ex, "PSScriptMissingHelpContentCommentBlock", ErrorCategory.ParserError, null); - parseMetadataErrors.Add(psScriptMissingHelpContentCommentBlockError); - errors = parseMetadataErrors.ToArray(); - return false; - } - - if (!String.IsNullOrEmpty(scriptCommentInfo.Description) && !scriptCommentInfo.Description.Contains("<#") && !scriptCommentInfo.Description.Contains("#>")) - { - parsedMetadata.Add("description", scriptCommentInfo.Description); - } - else - { - var message = String.Format("PSScript is missing the required Description property or Description value contains '<#' or '#>' which is invalid"); - var ex = new ArgumentException(message); - var psScriptMissingDescriptionOrInvalidPropertyError = new ErrorRecord(ex, "MissingOrInvalidDescriptionInScriptMetadata", ErrorCategory.ParserError, null); - parseMetadataErrors.Add(psScriptMissingDescriptionOrInvalidPropertyError); - errors = parseMetadataErrors.ToArray(); - return false; - } - - - // Check that the mandatory properites for a script are there (version, author, guid, in addition to description) - if (!parsedMetadata.ContainsKey("version") || String.IsNullOrWhiteSpace(parsedMetadata["version"].ToString())) - { - var message = "No version was provided in the script metadata. Script metadata must specify a version, author, description, and Guid."; - var ex = new ArgumentException(message); - var MissingVersionInScriptMetadataError = new ErrorRecord(ex, "MissingVersionInScriptMetadata", ErrorCategory.InvalidData, null); - parseMetadataErrors.Add(MissingVersionInScriptMetadataError); - errors = parseMetadataErrors.ToArray(); - return false; - } - - if (!parsedMetadata.ContainsKey("author") || String.IsNullOrWhiteSpace(parsedMetadata["author"].ToString())) - { - var message = "No author was provided in the script metadata. Script metadata must specify a version, author, description, and Guid."; - var ex = new ArgumentException(message); - var MissingAuthorInScriptMetadataError = new ErrorRecord(ex, "MissingAuthorInScriptMetadata", ErrorCategory.InvalidData, null); - parseMetadataErrors.Add(MissingAuthorInScriptMetadataError); - errors = parseMetadataErrors.ToArray(); - return false; - } - - if (!parsedMetadata.ContainsKey("guid") || String.IsNullOrWhiteSpace(parsedMetadata["guid"].ToString())) - { - var message = "No guid was provided in the script metadata. Script metadata must specify a version, author, description, and Guid."; - var ex = new ArgumentException(message); - var MissingGuidInScriptMetadataError = new ErrorRecord(ex, "MissingGuidInScriptMetadata", ErrorCategory.InvalidData, null); - parseMetadataErrors.Add(MissingGuidInScriptMetadataError); - errors = parseMetadataErrors.ToArray(); - return false; - } - - errors = parseMetadataErrors.ToArray(); - return true; - } - private bool CheckDependenciesExist(Hashtable dependencies, string repositoryName) { // Check to see that all dependencies are in the repository diff --git a/test/InstallPSResource.Tests.ps1 b/test/InstallPSResource.Tests.ps1 index b9ab10495..21c15d783 100644 --- a/test/InstallPSResource.Tests.ps1 +++ b/test/InstallPSResource.Tests.ps1 @@ -8,6 +8,7 @@ Describe 'Test Install-PSResource for Module' { BeforeAll { $PSGalleryName = Get-PSGalleryName + $PSGalleryUri = Get-PSGalleryLocation $NuGetGalleryName = Get-NuGetGalleryName $testModuleName = "test_module" $testModuleName2 = "TestModule99" @@ -180,6 +181,29 @@ Describe 'Test Install-PSResource for Module' { ($env:PSModulePath).Contains($pkg.InstalledLocation) } + It "Install resource with companyname, copyright and repository source location and validate" { + Install-PSResource -Name $testModuleName -Version "5.2.5-alpha001" -Repository PSGallery -TrustRepository + $pkg = Get-PSResource $testModuleName + $pkg.Version | Should -Be "5.2.5" + $pkg.Prerelease | Should -Be "alpha001" + + $pkg.CompanyName | Should -Be "Anam" + $pkg.Copyright | Should -Be "(c) Anam Navied. All rights reserved." + $pkg.RepositorySourceLocation | Should -Be $PSGalleryUri + } + + + It "Install script with companyname, copyright, and repository source location and validate" { + Install-PSResource -Name "Install-VSCode" -Version "1.4.2" -Repository $PSGalleryName -TrustRepository + + $res = Get-PSResource "Install-VSCode" -Version "1.4.2" + $res.Name | Should -Be "Install-VSCode" + $res.Version | Should -Be "1.4.2.0" + $res.CompanyName | Should -Be "Microsoft Corporation" + $res.Copyright | Should -Be "(c) Microsoft Corporation" + $res.RepositorySourceLocation | Should -Be $PSGalleryUri + } + # Windows only It "Install resource under CurrentUser scope - Windows only" -Skip:(!(Get-IsWindows)) { Install-PSResource -Name $testModuleName -Repository $PSGalleryName -TrustRepository -Scope CurrentUser diff --git a/test/PublishPSResource.Tests.ps1 b/test/PublishPSResource.Tests.ps1 index f1b1baf22..f143c9391 100644 --- a/test/PublishPSResource.Tests.ps1 +++ b/test/PublishPSResource.Tests.ps1 @@ -378,6 +378,28 @@ Describe "Test Publish-PSResource" { (Get-ChildItem $script:repositoryPath).FullName | Should -Be $expectedPath } + It "should publish a script without lines in between comment blocks locally" { + $scriptName = "ScriptWithoutEmptyLinesBetweenCommentBlocks" + $scriptVersion = "1.0.0" + $scriptPath = (Join-Path -Path $script:testScriptsFolderPath -ChildPath "$scriptName.ps1") + + Publish-PSResource -Path $scriptPath + + $expectedPath = Join-Path -Path $script:repositoryPath -ChildPath "$scriptName.$scriptVersion.nupkg" + (Get-ChildItem $script:repositoryPath).FullName | Should -Be $expectedPath + } + + It "should publish a script without lines in help block locally" { + $scriptName = "ScriptWithoutEmptyLinesInMetadata" + $scriptVersion = "1.0.0" + $scriptPath = (Join-Path -Path $script:testScriptsFolderPath -ChildPath "$scriptName.ps1") + + Publish-PSResource -Path $scriptPath + + $expectedPath = Join-Path -Path $script:repositoryPath -ChildPath "$scriptName.$scriptVersion.nupkg" + (Get-ChildItem $script:repositoryPath).FullName | Should -Be $expectedPath + } + It "should write error and not publish script when Author property is missing" { $scriptName = "InvalidScriptMissingAuthor.ps1" $scriptVersion = "1.0.0" @@ -385,7 +407,7 @@ Describe "Test Publish-PSResource" { $scriptFilePath = Join-Path $script:testScriptsFolderPath -ChildPath $scriptName Publish-PSResource -Path $scriptFilePath -ErrorVariable err -ErrorAction SilentlyContinue $err.Count | Should -Not -Be 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "MissingAuthorInScriptMetadata,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly "psScriptMissingAuthor,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" $publishedPath = Join-Path -Path $script:repositoryPath -ChildPath "$scriptName.$scriptVersion.nupkg" Test-Path -Path $publishedPath | Should -Be $false @@ -397,7 +419,7 @@ Describe "Test Publish-PSResource" { $scriptFilePath = Join-Path $script:testScriptsFolderPath -ChildPath $scriptName Publish-PSResource -Path $scriptFilePath -ErrorVariable err -ErrorAction SilentlyContinue $err.Count | Should -Not -Be 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "MissingVersionInScriptMetadata,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly "psScriptMissingVersion,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" $publishedPkgs = Get-ChildItem -Path $script:repositoryPath -Filter *.nupkg $publishedPkgs.Count | Should -Be 0 @@ -410,7 +432,7 @@ Describe "Test Publish-PSResource" { $scriptFilePath = Join-Path $script:testScriptsFolderPath -ChildPath $scriptName Publish-PSResource -Path $scriptFilePath -ErrorVariable err -ErrorAction SilentlyContinue $err.Count | Should -Not -Be 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "MissingGuidInScriptMetadata,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly "psScriptMissingGuid,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" $publishedPath = Join-Path -Path $script:repositoryPath -ChildPath "$scriptName.$scriptVersion.nupkg" Test-Path -Path $publishedPath | Should -Be $false @@ -423,7 +445,7 @@ Describe "Test Publish-PSResource" { $scriptFilePath = Join-Path $script:testScriptsFolderPath -ChildPath $scriptName Publish-PSResource -Path $scriptFilePath -ErrorVariable err -ErrorAction SilentlyContinue $err.Count | Should -Not -Be 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "MissingOrInvalidDescriptionInScriptMetadata,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly "PSScriptInfoMissingDescription,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" $publishedPath = Join-Path -Path $script:repositoryPath -ChildPath "$scriptName.$scriptVersion.nupkg" Test-Path -Path $publishedPath | Should -Be $false @@ -437,7 +459,7 @@ Describe "Test Publish-PSResource" { $scriptFilePath = Join-Path $script:testScriptsFolderPath -ChildPath $scriptName Publish-PSResource -Path $scriptFilePath -ErrorVariable err -ErrorAction SilentlyContinue $err.Count | Should -Not -Be 0 - $err[0].FullyQualifiedErrorId | Should -BeExactly "PSScriptMissingHelpContentCommentBlock,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" + $err[0].FullyQualifiedErrorId | Should -BeExactly "missingHelpInfoCommentError,Microsoft.PowerShell.PowerShellGet.Cmdlets.PublishPSResource" $publishedPath = Join-Path -Path $script:repositoryPath -ChildPath "$scriptName.$scriptVersion.nupkg" Test-Path -Path $publishedPath | Should -Be $false