commands.ps1
<# .SYNOPSIS Copy files with filter parameters .DESCRIPTION Copy files with filter parameters .PARAMETER Source The source path of copying files .PARAMETER Target The destination path of copying files .PARAMETER Filter The filter parameter .EXAMPLE PS C:\> Copy-Filtered -Source "c:\temp\source" -Target "c:\temp\target" -Filter *.* This will build copy all the files to the destination folder .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Copy-Filtered { param ( [Parameter(Mandatory = $true)] [string] $Source, [Parameter(Mandatory = $true)] [string] $Target, [Parameter(Mandatory = $true)] [string[]] $Filter ) $ResolvedSource = Resolve-Path $Source $NormalizedSource = $ResolvedSource.Path.TrimEnd([IO.Path]::DirectorySeparatorChar) + [IO.Path]::DirectorySeparatorChar Get-ChildItem $Source -Include $Filter -Recurse | ForEach-Object { $RelativeItemSource = $_.FullName.Replace($NormalizedSource, '') $ItemTarget = Join-Path $Target $RelativeItemSource $ItemTargetDir = Split-Path $ItemTarget if (!(Test-Path $ItemTargetDir)) { [void](New-Item $ItemTargetDir -Type Directory) } Copy-Item $_.FullName $ItemTarget } } <# .SYNOPSIS Finds files using match patterns. .DESCRIPTION Determines the find root from a list of patterns. Performs the find and then applies the glob patterns. Supports interleaved exclude patterns. Unrooted patterns are rooted using defaultRoot, unless matchOptions.matchBase is specified and the pattern is a basename only. For matchBase cases, the defaultRoot is used as the find root. .PARAMETER DefaultRoot Default path to root unrooted patterns. Falls back to System.DefaultWorkingDirectory or current location. .PARAMETER Pattern Patterns to apply. Supports interleaved exclude patterns. .PARAMETER FindOptions When the FindOptions parameter is not specified, defaults to (New-FindOptions -FollowSymbolicLinksTrue). Following soft links is generally appropriate unless deleting files. .PARAMETER MatchOptions When the MatchOptions parameter is not specified, defaults to (New-MatchOptions -Dot -NoBrace -NoCase). .EXAMPLE PS C:\> Find-FSCPSMatch -DefaultRoot "c:\temp\PackagesLocalDirectory" -Pattern '*.*' -FindOptions FollowSymbolicLinksTrue This will return all files .NOTES This if refactored Find-VSTSMatch function #> function Find-FSCPSMatch { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectUsageOfAssignmentOperator", "")] [OutputType('System.Object[]')] [CmdletBinding()] param( [Parameter()] [string]$DefaultRoot, [Parameter()] [string[]]$Pattern, $FindOptions, $MatchOptions) begin{ Invoke-TimeSignal -Start $ErrorActionPreference = 'Stop' Write-PSFMessage -Level Verbose -Message "DefaultRoot: '$DefaultRoot'" ##===========================internal functions start==========================## function New-FindOptions { [CmdletBinding()] param( [switch]$FollowSpecifiedSymbolicLink, [switch]$FollowSymbolicLinks) return New-Object psobject -Property @{ FollowSpecifiedSymbolicLink = $FollowSpecifiedSymbolicLink.IsPresent FollowSymbolicLinks = $FollowSymbolicLinks.IsPresent } } function New-MatchOptions { [CmdletBinding()] param( [switch]$Dot, [switch]$FlipNegate, [switch]$MatchBase, [switch]$NoBrace, [switch]$NoCase, [switch]$NoComment, [switch]$NoExt, [switch]$NoGlobStar, [switch]$NoNegate, [switch]$NoNull) return New-Object psobject -Property @{ Dot = $Dot.IsPresent FlipNegate = $FlipNegate.IsPresent MatchBase = $MatchBase.IsPresent NoBrace = $NoBrace.IsPresent NoCase = $NoCase.IsPresent NoComment = $NoComment.IsPresent NoExt = $NoExt.IsPresent NoGlobStar = $NoGlobStar.IsPresent NoNegate = $NoNegate.IsPresent NoNull = $NoNull.IsPresent } } function ConvertTo-NormalizedSeparators { [CmdletBinding()] param([string]$Path) # Convert slashes. $Path = "$Path".Replace('/', '\') # Remove redundant slashes. $isUnc = $Path -match '^\\\\+[^\\]' $Path = $Path -replace '\\\\+', '\' if ($isUnc) { $Path = '\' + $Path } return $Path } function Get-FindInfoFromPattern { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$DefaultRoot, [Parameter(Mandatory = $true)] [string]$Pattern, [Parameter(Mandatory = $true)] $MatchOptions) if (!$MatchOptions.NoBrace) { throw "Get-FindInfoFromPattern expected MatchOptions.NoBrace to be true." } # For the sake of determining the find path, pretend NoCase=false. $MatchOptions = Copy-MatchOptions -Options $MatchOptions $MatchOptions.NoCase = $false # Check if basename only and MatchBase=true if ($MatchOptions.MatchBase -and !(Test-Rooted -Path $Pattern) -and ($Pattern -replace '\\', '/').IndexOf('/') -lt 0) { return New-Object psobject -Property @{ AdjustedPattern = $Pattern FindPath = $DefaultRoot StatOnly = $false } } # The technique applied by this function is to use the information on the Minimatch object determine # the findPath. Minimatch breaks the pattern into path segments, and exposes information about which # segments are literal vs patterns. # # Note, the technique currently imposes a limitation for drive-relative paths with a glob in the # first segment, e.g. C:hello*/world. It's feasible to overcome this limitation, but is left unsolved # for now. $minimatchObj = New-Object Minimatch.Minimatcher($Pattern, (ConvertTo-MinimatchOptions -Options $MatchOptions)) # The "set" field is a two-dimensional enumerable of parsed path segment info. The outer enumerable should only # contain one item, otherwise something went wrong. Brace expansion can result in multiple items in the outer # enumerable, but that should be turned off by the time this function is reached. # # Note, "set" is a private field in the .NET implementation but is documented as a feature in the nodejs # implementation. The .NET implementation is a port and is by a different author. $setFieldInfo = $minimatchObj.GetType().GetField('set', 'Instance,NonPublic') [object[]]$set = $setFieldInfo.GetValue($minimatchObj) if ($set.Count -ne 1) { throw "Get-FindInfoFromPattern expected Minimatch.Minimatcher(...).set.Count to be 1. Actual: '$($set.Count)'" } [string[]]$literalSegments = @( ) [object[]]$parsedSegments = $set[0] foreach ($parsedSegment in $parsedSegments) { if ($parsedSegment.GetType().Name -eq 'LiteralItem') { # The item is a LiteralItem when the original input for the path segment does not contain any # unescaped glob characters. $literalSegments += $parsedSegment.Source; continue } break; } # Join the literal segments back together. Minimatch converts '\' to '/' on Windows, then squashes # consequetive slashes, and finally splits on slash. This means that UNC format is lost, but can # be detected from the original pattern. $joinedSegments = [string]::Join('/', $literalSegments) if ($joinedSegments -and ($Pattern -replace '\\', '/').StartsWith('//')) { $joinedSegments = '/' + $joinedSegments # restore UNC format } # Determine the find path. $findPath = '' if ((Test-Rooted -Path $Pattern)) { # The pattern is rooted. $findPath = $joinedSegments } elseif ($joinedSegments) { # The pattern is not rooted, and literal segements were found. $findPath = [System.IO.Path]::Combine($DefaultRoot, $joinedSegments) } else { # The pattern is not rooted, and no literal segements were found. $findPath = $DefaultRoot } # Clean up the path. if ($findPath) { $findPath = [System.IO.Path]::GetDirectoryName(([System.IO.Path]::Combine($findPath, '_'))) # Hack to remove unnecessary trailing slash. $findPath = ConvertTo-NormalizedSeparators -Path $findPath } return New-Object psobject -Property @{ AdjustedPattern = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $Pattern FindPath = $findPath StatOnly = $literalSegments.Count -eq $parsedSegments.Count } } function Get-FindResult { [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] $Options) if (!(Test-Path -LiteralPath $Path)) { Write-PSFMessage -Level Verbose -Message 'Path not found.' return } $Path = ConvertTo-NormalizedSeparators -Path $Path # Push the first item. [System.Collections.Stack]$stack = New-Object System.Collections.Stack $stack.Push((Get-Item -LiteralPath $Path)) $count = 0 while ($stack.Count) { # Pop the next item and yield the result. $item = $stack.Pop() $count++ $item.FullName # Traverse. if (($item.Attributes -band 0x00000010) -eq 0x00000010) { # Directory if (($item.Attributes -band 0x00000400) -ne 0x00000400 -or # ReparsePoint $Options.FollowSymbolicLinks -or ($count -eq 1 -and $Options.FollowSpecifiedSymbolicLink)) { $childItems = @( Get-ChildItem -Path "$($Item.FullName)/*" -Force ) [System.Array]::Reverse($childItems) foreach ($childItem in $childItems) { $stack.Push($childItem) } } } } } function Get-RootedPattern { [OutputType('System.String')] [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$DefaultRoot, [Parameter(Mandatory = $true)] [string]$Pattern) if ((Test-Rooted -Path $Pattern)) { return $Pattern } # Normalize root. $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot # Escape special glob characters. $DefaultRoot = $DefaultRoot -replace '(\[)(?=[^\/]+\])', '[[]' # Escape '[' when ']' follows within the path segment $DefaultRoot = $DefaultRoot.Replace('?', '[?]') # Escape '?' $DefaultRoot = $DefaultRoot.Replace('*', '[*]') # Escape '*' $DefaultRoot = $DefaultRoot -replace '\+\(', '[+](' # Escape '+(' $DefaultRoot = $DefaultRoot -replace '@\(', '[@](' # Escape '@(' $DefaultRoot = $DefaultRoot -replace '!\(', '[!](' # Escape '!(' if ($DefaultRoot -like '[A-Z]:') { # e.g. C: return "$DefaultRoot$Pattern" } # Ensure root ends with a separator. if (!$DefaultRoot.EndsWith('\')) { $DefaultRoot = "$DefaultRoot\" } return "$DefaultRoot$Pattern" } function Test-Rooted { [OutputType('System.Boolean')] [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path) $Path = ConvertTo-NormalizedSeparators -Path $Path return $Path.StartsWith('\') -or # e.g. \ or \hello or \\hello $Path -like '[A-Z]:*' # e.g. C: or C:\hello } function Copy-MatchOptions { [CmdletBinding()] param($Options) return New-Object psobject -Property @{ Dot = $Options.Dot -eq $true FlipNegate = $Options.FlipNegate -eq $true MatchBase = $Options.MatchBase -eq $true NoBrace = $Options.NoBrace -eq $true NoCase = $Options.NoCase -eq $true NoComment = $Options.NoComment -eq $true NoExt = $Options.NoExt -eq $true NoGlobStar = $Options.NoGlobStar -eq $true NoNegate = $Options.NoNegate -eq $true NoNull = $Options.NoNull -eq $true } } function ConvertTo-MinimatchOptions { [CmdletBinding()] param($Options) $opt = New-Object Minimatch.Options $opt.AllowWindowsPaths = $true $opt.Dot = $Options.Dot -eq $true $opt.FlipNegate = $Options.FlipNegate -eq $true $opt.MatchBase = $Options.MatchBase -eq $true $opt.NoBrace = $Options.NoBrace -eq $true $opt.NoCase = $Options.NoCase -eq $true $opt.NoComment = $Options.NoComment -eq $true $opt.NoExt = $Options.NoExt -eq $true $opt.NoGlobStar = $Options.NoGlobStar -eq $true $opt.NoNegate = $Options.NoNegate -eq $true $opt.NoNull = $Options.NoNull -eq $true return $opt } function Get-LocString { [OutputType('System.String')] [CmdletBinding()] param( [Parameter(Mandatory = $true, Position = 1)] [string]$Key, [Parameter(Position = 2)] [object[]]$ArgumentList = @( )) # Due to the dynamically typed nature of PowerShell, a single null argument passed # to an array parameter is interpreted as a null array. if ([object]::ReferenceEquals($null, $ArgumentList)) { $ArgumentList = @( $null ) } # Lookup the format string. $format = '' if (!($format = $script:resourceStrings[$Key])) { # Warn the key was not found. Prevent recursion if the lookup key is the # "string resource key not found" lookup key. $resourceNotFoundKey = 'PSLIB_StringResourceKeyNotFound0' if ($key -ne $resourceNotFoundKey) { Write-PSFMessage -Level Warning -Message (Get-LocString -Key $resourceNotFoundKey -ArgumentList $Key) } # Fallback to just the key itself if there aren't any arguments to format. if (!$ArgumentList.Count) { return $key } # Otherwise fallback to the key followed by the arguments. $OFS = " " return "$key $ArgumentList" } # Return the string if there aren't any arguments to format. if (!$ArgumentList.Count) { return $format } try { [string]::Format($format, $ArgumentList) } catch { Write-PSFMessage -Level Warning -Message (Get-LocString -Key 'PSLIB_StringFormatFailed') $OFS = " " "$format $ArgumentList" } } function ConvertFrom-LongFormPath { [OutputType('System.String')] [CmdletBinding()] param([string]$Path) if ($Path) { if ($Path.StartsWith('\\?\UNC')) { # E.g. \\?\UNC\server\share -> \\server\share return $Path.Substring(1, '\?\UNC'.Length) } elseif ($Path.StartsWith('\\?\')) { # E.g. \\?\C:\directory -> C:\directory return $Path.Substring('\\?\'.Length) } } return $Path } function ConvertTo-LongFormPath { [OutputType('System.String')] [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path) [string]$longFormPath = Get-FullNormalizedPath -Path $Path if ($longFormPath -and !$longFormPath.StartsWith('\\?')) { if ($longFormPath.StartsWith('\\')) { # E.g. \\server\share -> \\?\UNC\server\share return "\\?\UNC$($longFormPath.Substring(1))" } else { # E.g. C:\directory -> \\?\C:\directory return "\\?\$longFormPath" } } return $longFormPath } function Get-FullNormalizedPath { [OutputType('System.String')] [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string]$Path) [string]$outPath = $Path [uint32]$bufferSize = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, 0, $null, $null) [int]$lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() if ($bufferSize -gt 0) { $absolutePath = New-Object System.Text.StringBuilder([int]$bufferSize) [uint32]$length = [VstsTaskSdk.FS.NativeMethods]::GetFullPathName($Path, $bufferSize, $absolutePath, $null) $lastWin32Error = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error() if ($length -gt 0) { $outPath = $absolutePath.ToString() } else { throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( $lastWin32Error Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path )) } } else { throw (New-Object -TypeName System.ComponentModel.Win32Exception -ArgumentList @( $lastWin32Error Get-LocString -Key PSLIB_PathLengthNotReturnedFor0 -ArgumentList $Path )) } if ($outPath.EndsWith('\') -and !$outPath.EndsWith(':\')) { $outPath = $outPath.TrimEnd('\') } $outPath } ##===========================internal functions end============================## } process { try { if (!$FindOptions) { $FindOptions = New-FindOptions -FollowSpecifiedSymbolicLink -FollowSymbolicLinks } if (!$MatchOptions) { $MatchOptions = New-MatchOptions -Dot -NoBrace -NoCase } $miscFolder = (Join-Path $script:ModuleRoot "\internal\misc") [string]$code = Get-Content "$miscFolder\Minimatch.cs" -Raw Add-Type -TypeDefinition $code -Language CSharp # Normalize slashes for root dir. $DefaultRoot = ConvertTo-NormalizedSeparators -Path $DefaultRoot $results = @{ } $originalMatchOptions = $MatchOptions foreach ($pat in $Pattern) { Write-PSFMessage -Level Verbose -Message "Pattern: '$pat'" # Trim and skip empty. $pat = "$pat".Trim() if (!$pat) { Write-PSFMessage -Level Verbose -Message 'Skipping empty pattern.' continue } # Clone match options. $MatchOptions = Copy-MatchOptions -Options $originalMatchOptions # Skip comments. if (!$MatchOptions.NoComment -and $pat.StartsWith('#')) { Write-PSFMessage -Level Verbose -Message 'Skipping comment.' continue } # Set NoComment. Brace expansion could result in a leading '#'. $MatchOptions.NoComment = $true # Determine whether pattern is include or exclude. $negateCount = 0 if (!$MatchOptions.NoNegate) { while ($negateCount -lt $pat.Length -and $pat[$negateCount] -eq '!') { $negateCount++ } $pat = $pat.Substring($negateCount) # trim leading '!' if ($negateCount) { Write-PSFMessage -Level Verbose -Message "Trimmed leading '!'. Pattern: '$pat'" } } $isIncludePattern = $negateCount -eq 0 -or ($negateCount % 2 -eq 0 -and !$MatchOptions.FlipNegate) -or ($negateCount % 2 -eq 1 -and $MatchOptions.FlipNegate) # Set NoNegate. Brace expansion could result in a leading '!'. $MatchOptions.NoNegate = $true $MatchOptions.FlipNegate = $false # Trim and skip empty. $pat = "$pat".Trim() if (!$pat) { Write-PSFMessage -Level Verbose -Message 'Skipping empty pattern.' continue } # Expand braces - required to accurately interpret findPath. $expanded = $null $preExpanded = $pat if ($MatchOptions.NoBrace) { $expanded = @( $pat ) } else { # Convert slashes on Windows before calling braceExpand(). Unfortunately this means braces cannot # be escaped on Windows, this limitation is consistent with current limitations of minimatch (3.0.3). Write-PSFMessage -Level Verbose -Message "Expanding braces." $convertedPattern = $pat -replace '\\', '/' $expanded = [Minimatch.Minimatcher]::BraceExpand( $convertedPattern, (ConvertTo-MinimatchOptions -Options $MatchOptions)) } # Set NoBrace. $MatchOptions.NoBrace = $true foreach ($pat in $expanded) { if ($pat -ne $preExpanded) { Write-PSFMessage -Level Verbose -Message "Pattern: '$pat'" } # Trim and skip empty. $pat = "$pat".Trim() if (!$pat) { Write-PSFMessage -Level Verbose -Message "Skipping empty pattern." continue } if ($isIncludePattern) { # Determine the findPath. $findInfo = Get-FindInfoFromPattern -DefaultRoot $DefaultRoot -Pattern $pat -MatchOptions $MatchOptions $findPath = $findInfo.FindPath Write-PSFMessage -Level Verbose -Message "FindPath: '$findPath'" if (!$findPath) { Write-PSFMessage -Level Verbose -Message "Skipping empty path." continue } # Perform the find. Write-PSFMessage -Level Verbose -Message "StatOnly: '$($findInfo.StatOnly)'" [string[]]$findResults = @( ) if ($findInfo.StatOnly) { # Simply stat the path - all path segments were used to build the path. if ((Test-Path -LiteralPath $findPath)) { $findResults += $findPath } } else { $findResults = Get-FindResult -Path $findPath -Options $FindOptions } Write-PSFMessage -Level Verbose -Message "Found $($findResults.Count) paths." # Apply the pattern. Write-PSFMessage -Level Verbose -Message "Applying include pattern." if ($findInfo.AdjustedPattern -ne $pat) { Write-PSFMessage -Level Verbose -Message "AdjustedPattern: '$($findInfo.AdjustedPattern)'" $pat = $findInfo.AdjustedPattern } $matchResults = [Minimatch.Minimatcher]::Filter( $findResults, $pat, (ConvertTo-MinimatchOptions -Options $MatchOptions)) # Union the results. $matchCount = 0 foreach ($matchResult in $matchResults) { $matchCount++ $results[$matchResult.ToUpperInvariant()] = $matchResult } Write-PSFMessage -Level Verbose -Message "$matchCount matches" } else { # Check if basename only and MatchBase=true. if ($MatchOptions.MatchBase -and !(Test-Rooted -Path $pat) -and ($pat -replace '\\', '/').IndexOf('/') -lt 0) { # Do not root the pattern. Write-PSFMessage -Level Verbose -Message "MatchBase and basename only." } else { # Root the exclude pattern. $pat = Get-RootedPattern -DefaultRoot $DefaultRoot -Pattern $pat Write-PSFMessage -Level Verbose -Message "After Get-RootedPattern, pattern: '$pat'" } # Apply the pattern. Write-PSFMessage -Level Verbose -Message 'Applying exclude pattern.' $matchResults = [Minimatch.Minimatcher]::Filter( [string[]]$results.Values, $pat, (ConvertTo-MinimatchOptions -Options $MatchOptions)) # Subtract the results. $matchCount = 0 foreach ($matchResult in $matchResults) { $matchCount++ $results.Remove($matchResult.ToUpperInvariant()) } Write-PSFMessage -Level Verbose -Message "$matchCount matches" } } } $finalResult = @( $results.Values | Sort-Object ) Write-PSFMessage -Level Verbose -Message "$($finalResult.Count) final results" return $finalResult } catch { Write-PSFMessage -Level Host -Message "Something went wrong while finding-matches" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } finally{ } } END { Invoke-TimeSignal -End } } <# .SYNOPSIS Load all necessary information about the D365 instance .DESCRIPTION Load all servicing dll files from the D365 instance into memory .EXAMPLE PS C:\> Get-ApplicationEnvironment This will load all the different dll files into memory. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-ApplicationEnvironment { [System.Collections.ArrayList] $Files2Process = New-Object -TypeName "System.Collections.ArrayList" $AOSPath = Join-Path $script:ServiceDrive "\AOSService\webroot\bin" Write-PSFMessage -Level Verbose -Message "AOSPath $AOSPath" Write-PSFMessage -Level Verbose -Message "Testing if we are running on a AOS server or not." if (-not (Test-Path -Path $AOSPath -PathType Container)) { Write-PSFMessage -Level Verbose -Message "The machine is NOT an AOS server." $MRPath = Join-Path $script:ServiceDrive "MRProcessService\MRInstallDirectory\Server\Services" Write-PSFMessage -Level Verbose -Message "Testing if we are running on a BI / MR server or not." if (-not (Test-Path -Path $MRPath -PathType Container)) { Write-PSFMessage -Level Verbose -Message "It seems that you ran this cmdlet on a machine that doesn't have the assemblies needed to obtain system details. Most likely you ran it on a <c='em'>personal workstation / personal computer</c>." return } else { Write-PSFMessage -Level Verbose -Message "The machine is a BI / MR server." $BasePath = $MRPath $null = $Files2Process.Add((Join-Path $script:ServiceDrive "Monitoring\Instrumentation\Microsoft.Dynamics.AX.Authentication.Instrumentation.dll")) } } else { Write-PSFMessage -Level Verbose -Message "The machine is an AOS server." $BasePath = $AOSPath $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Authentication.Instrumentation.dll")) } Write-PSFMessage -Level Verbose -Message "Shadow cloning all relevant assemblies to the Microsoft.Dynamics.ApplicationPlatform.Environment.dll to avoid locking issues. This enables us to install updates while having fscps.tools loaded" $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Configuration.Base.dll")) $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.BusinessPlatform.SharedTypes.dll")) $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Framework.EncryptionEngine.dll")) $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.AX.Security.Instrumentation.dll")) $null = $Files2Process.Add((Join-Path $BasePath "Microsoft.Dynamics.ApplicationPlatform.Environment.dll")) Import-AssemblyFileIntoMemory -Path $($Files2Process.ToArray()) -UseTempFolder if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Level Verbose -Message "All assemblies loaded. Getting environment details." $environment = [Microsoft.Dynamics.ApplicationPlatform.Environment.EnvironmentFactory]::GetApplicationEnvironment() $environment } <# .SYNOPSIS Function to receive the Name of the model from descriptor .DESCRIPTION Function to receive the Name of the model from descriptor .PARAMETER _modelName Model name .PARAMETER _modelPath Model path .EXAMPLE PS C:\> Get-AXModelName ModelName "TestModel" ModelPath "c:\Temp\PackagesLocalDirectory" This will return the model name from descriptor .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-AXModelName { param ( [Alias('ModelName')] [string]$_modelName, [Alias('ModelPath')] [string]$_modelPath ) process{ $descriptorSearchPath = (Join-Path $_modelPath (Join-Path $_modelName "Descriptor")) if(Test-Path $descriptorSearchPath) { $descriptor = (Get-ChildItem -Path $descriptorSearchPath -Filter '*.xml') Write-PSFMessage -Level Verbose -Message "Descriptor found at $descriptor" [xml]$xmlData = Get-Content $descriptor.FullName $modelDisplayName = $xmlData.SelectNodes("//AxModelInfo/Name") return $modelDisplayName.InnerText } else { return $null; } } } <# .SYNOPSIS Clone a hashtable .DESCRIPTION Create a deep clone of a hashtable for you to work on it without updating the original object .PARAMETER InputObject The hashtable you want to clone .EXAMPLE PS C:\> Get-DeepClone -InputObject $HashTable This will clone the $HashTable variable into a new object and return it to you. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-DeepClone { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseOutputTypeCorrectly', '')] [CmdletBinding()] param( [parameter(Mandatory = $true)] $InputObject ) process { if($InputObject -is [hashtable]) { $clone = @{} foreach($key in $InputObject.keys) { if($key -eq "EnableException") {continue} $clone[$key] = Get-DeepClone $InputObject[$key] } $clone } else { $InputObject } } } <# .SYNOPSIS Get list of the D365FSC models from metadata path .DESCRIPTION Get list of the D365FSC models from metadata path prepared to build .PARAMETER MetadataPath Path to the metadata folder (PackagesLocalDirectory) .PARAMETER IncludeTest Includes test models .PARAMETER All Return all models even without source code .EXAMPLE PS C:\> Get-FSCModels -MetadataPath "J:\AosService\PackagesLocalDirectory" This will return the list of models without test models and models without source code .EXAMPLE PS C:\> Get-FSCModels -MetadataPath "J:\AosService\PackagesLocalDirectory" -IncludeTest This will return the list of models with test models and models without source code .EXAMPLE PS C:\> Get-FSCModels -MetadataPath "J:\AosService\PackagesLocalDirectory" -IncludeTest -All This will return the list of all models .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCModelList { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $MetadataPath, [switch] $IncludeTest = $false, [switch] $All = $false ) if(Test-Path "$MetadataPath") { $modelsList = @() (Get-ChildItem -Directory "$MetadataPath") | ForEach-Object { $testModel = ($_.BaseName -match "Test") if ($testModel -and $IncludeTest) { $modelsList += ($_.BaseName) } if((Test-Path ("$MetadataPath/$($_.BaseName)/Descriptor")) -and !$testModel) { $modelsList += ($_.BaseName) } if(!(Test-Path ("$MetadataPath/$($_.BaseName)/Descriptor")) -and !$testModel -and $All) { $modelsList += ($_.BaseName) } } return $modelsList -join "," } else { Write-PSFMessage -Level Host -Message "Something went wrong while downloading NuGet package" -Exception "Folder $MetadataPath with metadata doesnot exists" Stop-PSFFunction -Message "Stopping because of errors" return } } <# .SYNOPSIS Get the list of D365FSC components versions .DESCRIPTION Get the list of D365FSC components versions (NuGets, Packages, Frameworks etc.) .PARAMETER ModelsList The list of D365FSC models .PARAMETER MetadataPath The path to the D365FSC metadata .EXAMPLE PS C:\> Get-FSCMTestModel -ModelsList "test" $MetadataPath "c:\temp\Metadata" This will show the list of test models. .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCMTestModel { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $ModelsList, [Parameter(Mandatory = $true)] [string] $MetadataPath ) begin{ $testModelsList = @() function Get-AXModelReference { [CmdletBinding()] param ( [string] $descriptorPath ) if(Test-Path "$descriptorPath") { [xml]$xmlData = Get-Content $descriptorPath $modelDisplayName = $xmlData.SelectNodes("//AxModelInfo/ModuleReferences") return $modelDisplayName.string } } } process{ $ModelsList.Split(",") | ForEach-Object { $modelName = $_ (Get-ChildItem -Path $MetadataPath) | ForEach-Object{ $mdlName = $_.BaseName if($mdlName -eq $modelName){ return; } $checkTest = $($mdlName.Contains("Test")) if(-not $checkTest){ return; } Write-PSFMessage -Level Debug -Message "ModelName: $mdlName" $descriptorSearchPath = (Join-Path $_.FullName "Descriptor") $descriptor = (Get-ChildItem -Path $descriptorSearchPath -Filter '*.xml') if($descriptor) { $refmodels = (Get-AXModelReference -descriptorPath $descriptor.FullName) Write-PSFMessage -Level Debug -Message "RefModels: $refmodels" foreach($ref in $refmodels) { if($modelName -eq $ref) { if(-not $testModelsList.Contains("$mdlName")) { $testModelsList += ("$mdlName") } } } } } } } end{ return $testModelsList -join "," } } <# .SYNOPSIS Imports a .NET dll file into memory .DESCRIPTION Imports a .NET dll file into memory, by creating a copy (temporary file) and imports it using reflection .PARAMETER Path Path to the dll file you want to import Accepts an array of strings .PARAMETER UseTempFolder Instruct the cmdlet to create the file copy in the default temp folder This switch can be used, if writing to the original folder is not wanted or not possible .EXAMPLE PS C:\> Import-AssemblyFileIntoMemory -Path "C:\AOSService\PackagesLocalDirectory\Bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll" This will create an new file named "C:\AOSService\PackagesLocalDirectory\Bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll_shawdow.dll" The new file is then imported into memory using .NET Reflection. After the file has been imported, it will be deleted from disk. .EXAMPLE PS C:\> Import-AssemblyFileIntoMemory -Path "C:\AOSService\PackagesLocalDirectory\Bin\Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll" -UseTempFolder This will create an new file named "Microsoft.Dynamics.BusinessPlatform.ProductInformation.Framework.dll_shawdow.dll" in the temp folder The new file is then imported into memory using .NET Reflection. After the file has been imported, it will be deleted from disk. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Import-AssemblyFileIntoMemory { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true, Position = 1)] [string[]] $Path, [switch] $UseTempFolder ) if (-not (Test-PathExists -Path $Path -Type Leaf)) { Stop-PSFFunction -Message "Stopping because unable to locate file." -StepsUpward 1 return } foreach ($itemPath in $Path) { if ($UseTempFolder) { $filename = Split-Path -Path $itemPath -Leaf $shadowClonePath = Join-Path $env:TEMP "$filename`_shadow.dll" } else { $shadowClonePath = "$itemPath`_shadow.dll" } try { Write-PSFMessage -Level Debug -Message "Cloning $itemPath to $shadowClonePath" Copy-Item -Path $itemPath -Destination $shadowClonePath -Force Write-PSFMessage -Level Debug -Message "Loading $shadowClonePath into memory" $null = [AppDomain]::CurrentDomain.Load(([System.IO.File]::ReadAllBytes($shadowClonePath))) } catch { Write-PSFMessage -Level Host -Message "Something went wrong while working against the database" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally { Write-PSFMessage -Level Debug -Message "Removing $shadowClonePath" Remove-Item -Path $shadowClonePath -Force -ErrorAction SilentlyContinue } } } <# .SYNOPSIS Init the Azure Storage config variables .DESCRIPTION Update the active Azure Storage config variables that the module will use as default values .EXAMPLE PS C:\> Init-AzureStorageDefault This will update the Azure Storage variables. .NOTES This initializes the default NugetStorage settings Author: Oleksandr Nikolaiev (@onikolaiev) #> function Init-AzureStorageDefault { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseApprovedVerbs", "")] [CmdletBinding()] [OutputType()] param ( ) Register-FSCPSAzureStorageConfig -ConfigStorageLocation "System" Add-FSCPSAzureStorageConfig -Name NuGetStorage -SAS $Script:NuGetStorageSASToken -AccountId $Script:NuGetStorageAccountName -Container $Script:NuGetStorageContainer -Force Add-FSCPSAzureStorageConfig -Name ModelStorage -SAS $Script:ModelCacheStorageSASToken -AccountId $Script:NuGetStorageAccountName -Container $Script:ModelsStorageContainer -Force } <# .SYNOPSIS Invoke the D365Commerce compilation .DESCRIPTION Invoke the D365Commerce compilation .PARAMETER Version The version of the D365Commerce used to build .PARAMETER SourcesPath The folder contains a metadata files with binaries .PARAMETER BuildFolderPath The destination build folder .PARAMETER Force Cleanup destination build folder befor build .EXAMPLE PS C:\> Invoke-FSCPSCompile -Version 10.0.39 Example output: BUILD_FOLDER_PATH : c:\temp\fscps.tools\_bld\10.0.39_build BUILD_LOG_FILE_PATH : C:\Users\Administrator\AppData\Local\Temp\ScaleUnit.sln.msbuild.log CSU_ZIP_PATH : c:\temp\fscps.tools\_bld\artifacts\CloudScaleUnitExtensionPackage.Master-ContosoForD365Commerce-10.0.39_20240530.48.zip HW_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.HardwareStation.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe SC_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.StoreCommerce.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe SU_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.ScaleUnit.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe PACKAGE_NAME : Master-ContosoForD365Commerce-10.0.39_20240530.48 ARTIFACTS_PATH : c:\temp\fscps.tools\_bld\artifacts ARTIFACTS_LIST : ["C:\\temp\\fscps.tools\\_bld\\artifacts\\CloudScaleUnitExtensionPackage.Master-ContosoForD365Commerce-10.0.39_20240530.48.zip", "C:\\temp\\fscps.tools\\_bld\\artifacts\\POS.2.2.63.1.nupkg", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.Commerce.Runtime.Master-ContosoForD365Commerce-10.2.2.63.1.nupkg", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.HardwareStation.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.ScaleUnit.2.2.63.1.nupkg", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.ScaleUnit.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.StoreCommerce.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe", "C:\\temp\\fscps.tools\\_bld\\artifacts\\ContosoAddressWebService.2.2.63.1.nupkg", "C:\\temp\\fscps.tools\\_bld\\artifacts\\ContosoWebService.2.2.63.1.nupkg"] This will build D365FSC package with version "10.0.39" to the Temp folder .EXAMPLE PS C:\> Invoke-FSCPSCompile -SourcesPath "D:\Sources\connector-d365-commerce\" Example output: BUILD_FOLDER_PATH : c:\temp\fscps.tools\_bld\10.0.39_build BUILD_LOG_FILE_PATH : C:\Users\Administrator\AppData\Local\Temp\ScaleUnit.sln.msbuild.log CSU_ZIP_PATH : c:\temp\fscps.tools\_bld\artifacts\CloudScaleUnitExtensionPackage.Master-ContosoForD365Commerce-10.0.39_20240530.48.zip HW_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.HardwareStation.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe SC_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.StoreCommerce.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe SU_INSTALLER_PATH : c:\temp\fscps.tools\_bld\artifacts\Contoso.ScaleUnit.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe PACKAGE_NAME : Master-ContosoForD365Commerce-10.0.39_20240530.48 ARTIFACTS_PATH : c:\temp\fscps.tools\_bld\artifacts ARTIFACTS_LIST : ["C:\\temp\\fscps.tools\\_bld\\artifacts\\CloudScaleUnitExtensionPackage.Master-ContosoForD365Commerce-10.0.39_20240530.48.zip", "C:\\temp\\fscps.tools\\_bld\\artifacts\\POS.2.2.63.1.nupkg", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.Commerce.Runtime.Master-ContosoForD365Commerce-10.2.2.63.1.nupkg", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.HardwareStation.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.ScaleUnit.2.2.63.1.nupkg", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.ScaleUnit.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe", "C:\\temp\\fscps.tools\\_bld\\artifacts\\Contoso.StoreCommerce.Installer.Master-ContosoForD365Commerce-10.0.39_20240530.48.exe", "C:\\temp\\fscps.tools\\_bld\\artifacts\\ContosoAddressWebService.2.2.63.1.nupkg", "C:\\temp\\fscps.tools\\_bld\\artifacts\\ContosoWebService.2.2.63.1.nupkg"] This will build D365FSC package with version "10.0.39" to the Temp folder .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-CommerceCompile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")] [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [string] $Version, [Parameter(Mandatory = $true)] [string] $SourcesPath, [string] $BuildFolderPath = (Join-Path $script:DefaultTempPath _bld), [switch] $Force ) BEGIN { Invoke-TimeSignal -Start $helperPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\scripts\helpers.ps1" -Resolve . ($helperPath) try{ $CMDOUT = @{ Verbose = If ($PSBoundParameters.Verbose -eq $true) { $true } else { $false }; Debug = If ($PSBoundParameters.Debug -eq $true) { $true } else { $false } } $responseObject = [Ordered]@{} Write-PSFMessage -Level Important -Message "//================= Reading current FSC-PS settings ============================//" $settings = Get-FSCPSSettings @CMDOUT Write-PSFMessage -Level Important -Message "Complete" #if($Force) #{ Write-PSFMessage -Level Important -Message "//================= Cleanup build folder =======================================//" Remove-Item $BuildFolderPath -Recurse -Force -ErrorAction SilentlyContinue Write-PSFMessage -Level Important -Message "Complete" #} if($settings.artifactsPath -eq "") { $artifactDirectory = (Join-Path $BuildFolderPath $settings.artifactsFolderName) } else { $artifactDirectory = $settings.artifactsPath } if (Test-Path -Path $artifactDirectory -ErrorAction SilentlyContinue) { Remove-Item -Path $artifactDirectory -Recurse -Force -ErrorAction SilentlyContinue } if (!(Test-Path -Path $artifactDirectory)) { $null = [System.IO.Directory]::CreateDirectory($artifactDirectory) } Get-ChildItem $artifactDirectory -Recurse if($Version -eq "") { $Version = $settings.buildVersion } if($Version -eq "") { throw "D365FSC Version should be specified." } # Gather version info #$versionData = Get-FSCPSVersionInfo -Version $Version @CMDOUT $SolutionBuildFolderPath = (Join-Path $BuildFolderPath "$($Version)_build") $responseObject.BUILD_FOLDER_PATH = $SolutionBuildFolderPath } catch { Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } finally{ } } PROCESS { if (Test-PSFFunctionInterrupt) { return } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 try { Write-PSFMessage -Level Important -Message "//================= Copy source files to the build folder ======================//" $null = Test-PathExists -Path $BuildFolderPath -Type Container -Create @CMDOUT $null = Test-PathExists -Path $SolutionBuildFolderPath -Type Container -Create @CMDOUT Copy-Item $SourcesPath\* -Destination $SolutionBuildFolderPath -Recurse -Force @CMDOUT Write-PSFMessage -Level Important -Message "Complete" Write-PSFMessage -Level Important -Message "//================= Build solution =============================================//" $msbuildpath = & "C:\Program Files (x86)\Microsoft Visual Studio\Installer\vswhere.exe" -products * -requires Microsoft.Component.MSBuild -property installationPath -latest $origLocation = Get-Location Set-Location $SolutionBuildFolderPath if($msbuildpath -ne "") { $msbuildexepath = Join-Path $msbuildpath "MSBuild\Current\Bin\MSBuild.exe" $msbuildresult = Invoke-MsBuild -Path (Join-Path $SolutionBuildFolderPath $settings.solutionName) -MsBuildParameters "/t:restore,rebuild /property:Configuration=Release /property:NuGetInteractive=true /property:BuildingInsideVisualStudio=false" -MsBuildFilePath "$msbuildexepath" -ShowBuildOutputInCurrentWindow -BypassVisualStudioDeveloperCommandPrompt @CMDOUT } else { $msbuildresult = Invoke-MsBuild -Path (Join-Path $SolutionBuildFolderPath $settings.solutionName) -MsBuildParameters "/t:restore,rebuild /property:Configuration=Release /property:NuGetInteractive=true /property:BuildingInsideVisualStudio=false" -ShowBuildOutputInCurrentWindow @CMDOUT } $responseObject.BUILD_LOG_FILE_PATH = $msbuildresult.BuildLogFilePath if ($msbuildresult.BuildSucceeded -eq $true) { Write-PSFMessage -Level Host -Message ("Build completed successfully in {0:N1} seconds." -f $msbuildresult.BuildDuration.TotalSeconds) } elseif ($msbuildresult.BuildSucceeded -eq $false) { throw ("Build failed after {0:N1} seconds. Check the build log file '$($msbuildresult.BuildLogFilePath)' for errors." -f $msbuildresult.BuildDuration.TotalSeconds) } elseif ($null -eq $msbuildresult.BuildSucceeded) { throw "Unsure if build passed or failed: $($msbuildresult.Message)" } Set-Location $origLocation if($settings.generatePackages) { Write-PSFMessage -Level Important -Message "//================= Generate package ==========================================//" switch ($settings.namingStrategy) { { $settings.namingStrategy -eq "Default" } { $packageNamePattern = $settings.packageNamePattern; if($settings.packageName.Contains('.zip')) { $packageName = $settings.packageName } else { $packageName = $settings.packageName } $packageNamePattern = $packageNamePattern.Replace("BRANCHNAME", $($settings.sourceBranch)) if($settings.deploy) { $packageNamePattern = $packageNamePattern.Replace("PACKAGENAME", $settings.azVMName) } else { $packageNamePattern = $packageNamePattern.Replace("PACKAGENAME", $packageName) } $packageNamePattern = $packageNamePattern.Replace("FNSCMVERSION", $Version) $packageNamePattern = $packageNamePattern.Replace("DATE", (Get-Date -Format "yyyyMMdd").ToString()) $packageNamePattern = $packageNamePattern.Replace("RUNNUMBER", $settings.runId) $packageName = $packageNamePattern break; } { $settings.namingStrategy -eq "Custom" } { if($settings.packageName.Contains('.zip')) { $packageName = $settings.packageName } else { $packageName = $settings.packageName + ".zip" } break; } Default { $packageName = $settings.packageName break; } } [System.IO.DirectoryInfo]$csuZipPackagePath = Get-ChildItem -Path $SolutionBuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*ScaleUnit.*.zip$"} | ForEach-Object {$_.FullName} [System.IO.DirectoryInfo]$hWSInstallerPath = Get-ChildItem -Path $SolutionBuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*HardwareStation.*.exe$"} | ForEach-Object {$_.FullName} [System.IO.DirectoryInfo]$sCInstallerPath = Get-ChildItem -Path $SolutionBuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*StoreCommerce.*.exe$"} | ForEach-Object {$_.FullName} [System.IO.DirectoryInfo]$sUInstallerPath = Get-ChildItem -Path $SolutionBuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*ScaleUnit.*.exe$"} | ForEach-Object {$_.FullName} Write-PSFMessage -Level Important -Message "//================= Copy packages to the artifacts folder ======================//" if($csuZipPackagePath) { Write-PSFMessage -Level Important -Message "CSU Package processing..." Write-PSFMessage -Level Important -Message $csuZipPackagePath if($settings.cleanupCSUPackage) { $null = [Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') $zipfile = $csuZipPackagePath $stream = New-Object IO.FileStream($zipfile, [IO.FileMode]::Open) $mode = [IO.Compression.ZipArchiveMode]::Update $zip = New-Object IO.Compression.ZipArchive($stream, $mode) ($zip.Entries | Where-Object { $_.Name -match 'Azure' }) | ForEach-Object { $_.Delete() } ($zip.Entries | Where-Object { $_.Name -match 'Microsoft' }) | ForEach-Object { $_.Delete() } ($zip.Entries | Where-Object { $_.Name -match 'System' -and $_.Name -notmatch 'System.Runtime.Caching' -and $_.Name -notmatch 'System.ServiceModel.Http' -and $_.Name -notmatch 'System.ServiceModel.Primitives' -and $_.Name -notmatch 'System.Private.ServiceModel' -and $_.Name -notmatch 'System.Configuration.ConfigurationManager' -and $_.Name -notmatch 'System.Security.Cryptography.ProtectedData' -and $_.Name -notmatch 'System.Security.Permissions' -and $_.Name -notmatch 'System.Security.Cryptography.Xml' -and $_.Name -notmatch 'System.Security.Cryptography.Pkcs' }) | ForEach-Object { $_.Delete() } ($zip.Entries | Where-Object { $_.Name -match 'Newtonsoft' }) | ForEach-Object { $_.Delete() } $zip.Dispose() $stream.Close() $stream.Dispose() } $destinationFullName = (Join-Path $($artifactDirectory) "$(ClearExtension($csuZipPackagePath)).$($packageName).zip") Copy-ToDestination -RelativePath $csuZipPackagePath.Parent.FullName -File $csuZipPackagePath.BaseName -DestinationFullName $destinationFullName $responseObject.CSU_ZIP_PATH = $destinationFullName } if($hWSInstallerPath) { Write-PSFMessage -Level Important -Message "HW Package processing..." Write-PSFMessage -Level Important -Message $hWSInstallerPath $destinationFullName = (Join-Path $($artifactDirectory) "$(ClearExtension($hWSInstallerPath)).$($packageName).exe") Copy-ToDestination -RelativePath $hWSInstallerPath.Parent.FullName -File $hWSInstallerPath.BaseName -DestinationFullName $destinationFullName $responseObject.HW_INSTALLER_PATH = $destinationFullName } if($sCInstallerPath) { Write-PSFMessage -Level Important -Message "SC Package processing..." Write-PSFMessage -Level Important -Message $sCInstallerPath $destinationFullName = (Join-Path $($artifactDirectory) "$(ClearExtension($sCInstallerPath)).$($packageName).exe") Copy-ToDestination -RelativePath $sCInstallerPath.Parent.FullName -File $sCInstallerPath.BaseName -DestinationFullName $destinationFullName $responseObject.SC_INSTALLER_PATH = $destinationFullName } if($sUInstallerPath) { Write-PSFMessage -Level Important -Message "SU Package processing..." Write-PSFMessage -Level Important -Message $sUInstallerPath $destinationFullName = (Join-Path $($artifactDirectory) "$(ClearExtension($sUInstallerPath)).$($packageName).exe") Copy-ToDestination -RelativePath $sUInstallerPath.Parent.FullName -File $sUInstallerPath.BaseName -DestinationFullName $destinationFullName $responseObject.SU_INSTALLER_PATH = $destinationFullName } Write-PSFMessage -Level Important -Message "//================= Export NuGets ===============================================//" Get-ChildItem -Path $BuildFolderPath -Recurse | Where-Object {$_.FullName -match "bin.*.Release.*.nupkg$"} | ForEach-Object { if($settings.cleanupNugets) { $zipfile = $_ # Cleanup NuGet file $null = [Reflection.Assembly]::LoadWithPartialName('System.IO.Compression') $stream = New-Object IO.FileStream($zipfile.FullName, [IO.FileMode]::Open) $mode = [IO.Compression.ZipArchiveMode]::Update $zip = New-Object IO.Compression.ZipArchive($stream, $mode) ($zip.Entries | Where-Object { $_.Name -match 'Azure' }) | ForEach-Object { $_.Delete() } ($zip.Entries | Where-Object { $_.Name -match 'Microsoft' }) | ForEach-Object { $_.Delete() } ($zip.Entries | Where-Object { $_.Name -match 'System' }) | ForEach-Object { $_.Delete() } ($zip.Entries | Where-Object { $_.Name -match 'Newtonsoft' }) | ForEach-Object { $_.Delete() } $zip.Dispose() $stream.Close() $stream.Dispose() } Copy-ToDestination -RelativePath $_.Directory -File $_.Name -DestinationFullName "$($artifactDirectory)\$($_.BaseName).nupkg" } $responseObject.PACKAGE_NAME = $packageName $responseObject.ARTIFACTS_PATH = $artifactDirectory $artifacts = Get-ChildItem $artifactDirectory $artifactsList = $artifacts.FullName -join "," if($artifactsList.Contains(',')) { $artifacts = $artifactsList.Split(',') | ConvertTo-Json -compress } else { $artifacts = '["'+$($artifactsList).ToString()+'"]' } $responseObject.ARTIFACTS_LIST = $artifacts } } catch { Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } finally{ try { if($SolutionBuildFolderPath) { if (Test-Path -Path $SolutionBuildFolderPath -ErrorAction SilentlyContinue) { Remove-Item -Path $SolutionBuildFolderPath -Recurse -Force -ErrorAction SilentlyContinue } } if($NuGetPackagesPath) { if (Test-Path -Path $NuGetPackagesPath -ErrorAction SilentlyContinue) { Remove-Item -Path $NuGetPackagesPath -Recurse -Force -ErrorAction SilentlyContinue } } if($outputDir) { if (Test-Path -Path $outputDir -ErrorAction SilentlyContinue) { Remove-Item -Path $outputDir -Recurse -Force -ErrorAction SilentlyContinue } } if($tempCombinedPackage) { if (Test-Path -Path $tempCombinedPackage -ErrorAction SilentlyContinue) { Remove-Item -Path $tempCombinedPackage -Recurse -Force -ErrorAction SilentlyContinue } } Set-Location $origLocation } catch { Write-PSFMessage -Level Verbose -Message "Cleanup warning: $($PSItem.Exception)" } $responseObject } } END { Invoke-TimeSignal -End } } <# .SYNOPSIS This will import D365FSC base assemblies .DESCRIPTION This will import D365FSC base assemblies. For package generating process .PARAMETER binDir XppTools directory path .EXAMPLE PS C:\> Invoke-FSCAssembliesImport -DefaultRoot "C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp.7.0.7120.99\ref\net40" .NOTES General notes #> function Invoke-FSCAssembliesImport([string]$binDir) { Write-PSFMessage -Level Verbose -Message "Importing metadata assemblies" # Need load metadata.dll and any referenced ones, not flexible to pick the new added references $m_core = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Core.dll $m_metadata = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.dll $m_storage = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Storage.dll $m_xppinstrumentation = Join-Path $binDir Microsoft.Dynamics.ApplicationPlatform.XppServices.Instrumentation.dll $m_management_core = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Management.Core.dll $m_management_delta = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Management.Delta.dll $m_management_diff = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Management.Diff.dll $m_management_merge = Join-Path $binDir Microsoft.Dynamics.AX.Metadata.Management.Merge.dll # Load required dlls, loading should fail the script run with exceptions thrown [Reflection.Assembly]::LoadFile($m_core) > $null [Reflection.Assembly]::LoadFile($m_metadata) > $null [Reflection.Assembly]::LoadFile($m_storage) > $null [Reflection.Assembly]::LoadFile($m_xppinstrumentation) > $null [Reflection.Assembly]::LoadFile($m_management_core) > $null [Reflection.Assembly]::LoadFile($m_management_delta) > $null [Reflection.Assembly]::LoadFile($m_management_diff) > $null [Reflection.Assembly]::LoadFile($m_management_merge) > $null } <# .SYNOPSIS Invoke the D365FSC models compilation .DESCRIPTION Invoke the D365FSC models compilation .PARAMETER Version The version of the D365FSC used to build .PARAMETER SourcesPath The folder contains a metadata files with binaries .PARAMETER BuildFolderPath The destination build folder .PARAMETER Force Cleanup destination build folder befor build .EXAMPLE PS C:\> Invoke-FSCCompile -Version "10.0.39" Example output: METADATA_DIRECTORY : D:\a\8\s\Metadata FRAMEWORK_DIRECTORY : C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.CompilerPackage.7.0.7120.99 BUILD_OUTPUT_DIRECTORY : C:\temp\buildbuild\bin NUGETS_FOLDER : C:\temp\buildbuild\packages BUILD_LOG_FILE_PATH : C:\Users\VssAdministrator\AppData\Local\Temp\Build.sln.msbuild.log PACKAGE_NAME : MAIN TEST-DeployablePackage-10.0.39-78 PACKAGE_PATH : C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip ARTIFACTS_PATH : C:\temp\buildbuild\artifacts ARTIFACTS_LIST : ["C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip"] This will build D365FSC package with version "10.0.39" to the Temp folder .EXAMPLE PS C:\> Invoke-FSCCompile -Version "10.0.39" -Path "c:\Temp" Example output: METADATA_DIRECTORY : D:\a\8\s\Metadata FRAMEWORK_DIRECTORY : C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.CompilerPackage.7.0.7120.99 BUILD_OUTPUT_DIRECTORY : C:\temp\buildbuild\bin NUGETS_FOLDER : C:\temp\buildbuild\packages BUILD_LOG_FILE_PATH : C:\Users\VssAdministrator\AppData\Local\Temp\Build.sln.msbuild.log PACKAGE_NAME : MAIN TEST-DeployablePackage-10.0.39-78 PACKAGE_PATH : C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip ARTIFACTS_PATH : C:\temp\buildbuild\artifacts ARTIFACTS_LIST : ["C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip"] This will build D365FSC package with version "10.0.39" to the Temp folder .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-FSCCompile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")] [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [string] $Version, [Parameter(Mandatory = $true)] [string] $SourcesPath, [string] $BuildFolderPath = (Join-Path $script:DefaultTempPath _bld), [switch] $Force ) BEGIN { Invoke-TimeSignal -Start try{ $helperPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\scripts\helpers.ps1" -Resolve . ($helperPath) $CMDOUT = @{ Verbose = If ($PSBoundParameters.Verbose -eq $true) { $true } else { $false }; Debug = If ($PSBoundParameters.Debug -eq $true) { $true } else { $false } } $responseObject = [Ordered]@{} Write-PSFMessage -Level Important -Message "//================= Reading current FSC-PS settings ============================//" Write-PSFMessage -Level Important -Message "IsOneBox: $($Script:IsOnebox)" if($Script:IsOnebox) { Write-PSFMessage -Level Important -Message "EnvironmentType: $($Script:EnvironmentType)" Write-PSFMessage -Level Important -Message "HostName: $($environment.Infrastructure.HostName)" Write-PSFMessage -Level Important -Message "AOSPath: $($Script:AOSPath)" Write-PSFMessage -Level Important -Message "DatabaseServer: $($Script:DatabaseServer)" Write-PSFMessage -Level Important -Message "PackageDirectory: $($Script:PackageDirectory)" Write-PSFMessage -Level Important -Message "BinDirTools: $($Script:BinDirTools)" Write-PSFMessage -Level Important -Message "MetaDataDir: $($Script:MetaDataDir)" } $settings = Get-FSCPSSettings @CMDOUT if([string]::IsNullOrEmpty($Version)) { $Version = $settings.buildVersion } if([string]::IsNullOrEmpty($Version)) { throw "D365FSC Version should be specified." } if([string]::IsNullOrEmpty($BuildFolderPath)) { $BuildFolderPath = (Join-Path $script:DefaultTempPath _bld) } if([string]::IsNullOrEmpty($settings.sourceBranch)) { $settings.sourceBranch = $settings.currentBranch } if([string]::IsNullOrEmpty($settings.artifactsPath)) { $artifactDirectory = (Join-Path $BuildFolderPath $settings.artifactsFolderName) } else { $artifactDirectory = $settings.artifactsPath } if (Test-Path -Path $artifactDirectory) { Remove-Item -Path $artifactDirectory -Recurse -Force $null = [System.IO.Directory]::CreateDirectory($artifactDirectory) } $buildLogsDirectory = (Join-Path $artifactDirectory "Logs") if (Test-Path -Path $buildLogsDirectory) { Remove-Item -Path $buildLogsDirectory -Recurse -Force $null = [System.IO.Directory]::CreateDirectory($buildLogsDirectory) } # Gather version info $versionData = Get-FSCPSVersionInfo -Version $Version @CMDOUT $PlatformVersion = $versionData.data.PlatformVersion $ApplicationVersion = $versionData.data.AppVersion $tools_package_name = 'Microsoft.Dynamics.AX.Platform.CompilerPackage.' + $PlatformVersion $plat_package_name = 'Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp.' + $PlatformVersion $app_package_name = 'Microsoft.Dynamics.AX.Application.DevALM.BuildXpp.' + $ApplicationVersion $appsuite_package_name = 'Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp.' + $ApplicationVersion $NuGetPackagesPath = (Join-Path $BuildFolderPath packages) $SolutionBuildFolderPath = (Join-Path $BuildFolderPath "$($Version)_build") $NuGetPackagesConfigFilePath = (Join-Path $SolutionBuildFolderPath packages.config) $NuGetConfigFilePath = (Join-Path $SolutionBuildFolderPath nuget.config) if(Test-Path "$($SourcesPath)/PackagesLocalDirectory") { $SourceMetadataPath = (Join-Path $($SourcesPath) "/PackagesLocalDirectory") } elseif(Test-Path "$($SourcesPath)/Metadata") { $SourceMetadataPath = (Join-Path $($SourcesPath) "/Metadata") } else { $SourceMetadataPath = $($SourcesPath) } $BuidPropsFile = (Join-Path $SolutionBuildFolderPath \Build\build.props) $msReferenceFolder = "$($NuGetPackagesPath)\$($app_package_name)\ref\net40;$($NuGetPackagesPath)\$($plat_package_name)\ref\net40;$($NuGetPackagesPath)\$($appsuite_package_name)\ref\net40;$($SourceMetadataPath);$($BuildFolderPath)\bin" $msBuildTasksDirectory = "$NuGetPackagesPath\$($tools_package_name)\DevAlm".Trim() $msMetadataDirectory = "$($SourceMetadataPath)".Trim() $msFrameworkDirectory = "$($NuGetPackagesPath)\$($tools_package_name)".Trim() $msReferencePath = "$($NuGetPackagesPath)\$($tools_package_name)".Trim() $msOutputDirectory = "$($BuildFolderPath)\bin".Trim() $responseObject.METADATA_DIRECTORY = $msMetadataDirectory $responseObject.FRAMEWORK_DIRECTORY = $msFrameworkDirectory $responseObject.BUILD_OUTPUT_DIRECTORY = $msOutputDirectory $responseObject.BUILD_FOLDER_PATH = $BuildFolderPath Write-PSFMessage -Level Important -Message "//================= Getting the list of models to build ========================//" if($($settings.specifyModelsManually) -eq "true") { $mtdtdPath = ("$($SourcesPath)\$($settings.metadataPath)".Trim()) $mdls = $($settings.models).Split(",") if($($settings.includeTestModel) -eq "true") { $testModels = Get-FSCMTestModel -modelNames $($mdls -join ",") -metadataPath $mtdtdPath @CMDOUT ($testModels.Split(",").ForEach({$mdls+=($_)})) } $models = $mdls -join "," $modelsToPackage = $models } else { $models = Get-FSCModelList -MetadataPath $SourceMetadataPath -IncludeTest:($settings.includeTestModel -eq 'true') @CMDOUT if($settings.enableBuildCaching) { Write-PSFMessage -Level Important -Message "Model caching is enabled." if(($settings.repoProvider -eq "GitHub") -or ($settings.repoProvider -eq "AzureDevOps")) { $modelsHash = [Ordered]@{} $modelsToCache = @() Write-PSFMessage -Level Important -Message "Running in $($settings.repoProvider). Start processing" foreach ($model in $models.Split(",")) { $modelName = $model Write-PSFMessage -Level Important -Message "Model: $modelName cache validation" $modelRootPath = (Join-Path $SourceMetadataPath $modelName ) $modelHash = Get-FolderHash $modelRootPath $modelsHash.$modelName = $modelHash $validation = Validate-FSCModelCache -MetadataDirectory $SourceMetadataPath -RepoOwner $settings.repoOwner -RepoName $settings.repoName -ModelName $modelName -Version $Version -BranchName $settings.sourceBranch if(-not $validation) { $modelsToCache += ($modelName) } } if($modelsToCache) { $modelsToBuild = $modelsToCache -join "," } } else { $modelsToBuild = $models } } else { $modelsToBuild = $models } $modelsToPackage = Get-FSCModelList -MetadataPath $SourceMetadataPath -IncludeTest:($settings.includeTestModel -eq 'true') -All @CMDOUT } if(-not $modelsToBuild){$modelsToBuild = ""} Write-PSFMessage -Level Important -Message "Models to build: $modelsToBuild" Write-PSFMessage -Level Important -Message "Models to package: $modelsToPackage" } catch { Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } finally{ } } PROCESS { if (Test-PSFFunctionInterrupt) { return } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 try { if($Force) { Write-PSFMessage -Level Important -Message "//================= Cleanup build folder =======================================//" Remove-Item $BuildFolderPath -Recurse -Force -ErrorAction SilentlyContinue } Write-PSFMessage -Level Important -Message "//================= Generate solution folder ===================================//" $null = Invoke-GenerateSolution -ModelsList $modelsToBuild -Version "$Version" -MetadataPath $SourceMetadataPath -SolutionFolderPath $BuildFolderPath @CMDOUT Write-PSFMessage -Level Important -Message "Complete" Write-PSFMessage -Level Important -Message "//================= Copy source files to the build folder ======================//" $null = Test-PathExists -Path $BuildFolderPath -Type Container -Create @CMDOUT $null = Test-PathExists -Path $SolutionBuildFolderPath -Type Container -Create @CMDOUT Write-PSFMessage -Level Important -Message "Source folder: $SourcesPath" Write-PSFMessage -Level Important -Message "Destination folder: $BuildFolderPath" Copy-Item $SourcesPath\* -Destination $BuildFolderPath -Recurse -Force @CMDOUT Write-PSFMessage -Level Important -Message "Complete" Write-PSFMessage -Level Important -Message "//================= Download NuGet packages ====================================//" $null = Test-PathExists -Path $NuGetPackagesPath -Type Container -Create @CMDOUT $null = Get-FSCPSNuget -Version $PlatformVersion -Type PlatformCompilerPackage -Path $NuGetPackagesPath -Force @CMDOUT $null = Get-FSCPSNuget -Version $PlatformVersion -Type PlatformDevALM -Path $NuGetPackagesPath -Force @CMDOUT $null = Get-FSCPSNuget -Version $ApplicationVersion -Type ApplicationDevALM -Path $NuGetPackagesPath -Force @CMDOUT $null = Get-FSCPSNuget -Version $ApplicationVersion -Type ApplicationSuiteDevALM -Path $NuGetPackagesPath -Force @CMDOUT Write-PSFMessage -Level Important -Message "Complete" $responseObject.NUGETS_FOLDER = $NuGetPackagesPath Write-PSFMessage -Level Important -Message "//================= Install NuGet packages =====================================//" #validata NuGet installation $nugetPath = Get-PSFConfigValue -FullName "fscps.tools.path.nuget" if(-not (Test-Path $nugetPath)) { Install-FSCPSNugetCLI } ##update nuget config file $nugetNewContent = (Get-Content $NuGetConfigFilePath).Replace('c:\temp\packages', $NuGetPackagesPath) Set-Content $NuGetConfigFilePath $nugetNewContent $null = (& $nugetPath restore $NuGetPackagesConfigFilePath -PackagesDirectory $NuGetPackagesPath -ConfigFile $NuGetConfigFilePath) Write-PSFMessage -Level Important -Message "Complete" Write-PSFMessage -Level Important -Message "//================= Copy binaries to the build folder ==========================//" Copy-Filtered -Source $SourceMetadataPath -Target (Join-Path $BuildFolderPath bin) -Filter *.* Write-PSFMessage -Level Important -Message "Complete" if($modelsToBuild) { Write-PSFMessage -Level Important -Message "//================= Build solution =============================================//" Set-Content $BuidPropsFile (Get-Content $BuidPropsFile).Replace('ReferenceFolders', $msReferenceFolder) $msbuildresult = Invoke-MsBuild -Path (Join-Path $SolutionBuildFolderPath "\Build\Build.sln") -P "/p:BuildTasksDirectory=$msBuildTasksDirectory /p:MetadataDirectory=$msMetadataDirectory /p:FrameworkDirectory=$msFrameworkDirectory /p:ReferencePath=$msReferencePath /p:OutputDirectory=$msOutputDirectory" -ShowBuildOutputInCurrentWindow @CMDOUT $responseObject.BUILD_LOG_FILE_PATH = $msbuildresult.BuildLogFilePath Copy-Filtered -Source (Join-Path $SolutionBuildFolderPath "Build") -Target $buildLogsDirectory -Filter *Dynamics.AX.*.xppc.* Copy-Filtered -Source (Join-Path $SolutionBuildFolderPath "Build") -Target $buildLogsDirectory -Filter *Dynamics.AX.*.labelc.* Copy-Filtered -Source (Join-Path $SolutionBuildFolderPath "Build") -Target $buildLogsDirectory -Filter *Dynamics.AX.*.reportsc.* Get-ChildItem -Path $buildLogsDirectory | ForEach-Object { if($_.Length -eq 0) {$_.Delete()}} if ($msbuildresult.BuildSucceeded -eq $true) { Write-PSFMessage -Level Host -Message ("Build completed successfully in {0:N1} seconds." -f $msbuildresult.BuildDuration.TotalSeconds) if($settings.enableBuildCaching) { Write-PSFMessage -Level Important -Message "//================= Upload cached models to the storageaccount ================//" foreach ($model in $modelsToBuild.Split(",")) { $modelName = $model $modelHash = $modelsHash.$modelName $modelBinPath = (Join-Path $msOutputDirectory $modelName) $modelFileNameWithHash = "$(($settings.repoOwner).ToLower())_$(($settings.repoName).ToLower())_$($modelName.ToLower())_$($settings.sourceBranch.ToLower())_$($Version)_$($modelHash).7z".Replace(" ", "-") $modelArchivePath = (Join-Path $BuildFolderPath $modelFileNameWithHash) $storageConfigs = Get-FSCPSAzureStorageConfig $activeStorageConfigName = "ModelStorage" if($storageConfigs) { $activeStorageConfig = Get-FSCPSActiveAzureStorageConfig $storageConfigs | ForEach-Object { if($_.AccountId -eq $activeStorageConfig.AccountId -and $_.Container -eq $activeStorageConfig.Container -and $_.SAS -eq $activeStorageConfig.SAS) { if($activeStorageConfigName) { $activeStorageConfigName = $_.Name } } } } Write-PSFMessage -Level Host -Message "Uploading compiled model binaries: $modelName" Write-PSFMessage -Level Host -Message "File: $modelFileNameWithHash" Compress-7zipArchive -Path $modelBinPath\* -DestinationPath $modelArchivePath Set-FSCPSActiveAzureStorageConfig ModelStorage $null = Invoke-FSCPSAzureStorageUpload -FilePath $modelArchivePath if(-not [string]::IsNullOrEmpty($activeStorageConfigName)){ Set-FSCPSActiveAzureStorageConfig $activeStorageConfigName } } Write-PSFMessage -Level Important -Message "Complete" } } elseif ($msbuildresult.BuildSucceeded -eq $false) { throw ("Build failed after {0:N1} seconds. Check the build log file '$($msbuildresult.BuildLogFilePath)' for errors." -f $msbuildresult.BuildDuration.TotalSeconds) } elseif ($null -eq $msbuildresult.BuildSucceeded) { throw "Unsure if build passed or failed: $($msbuildresult.Message)" } } if($settings.generatePackages) { if($PSVersionTable.PSVersion.Major -gt 5) { Write-PSFMessage -Level Warning -Message "Current PS version is $($PSVersionTable.PSVersion). The latest PS version acceptable to generate the D365FSC deployable package is 5." } else { Write-PSFMessage -Level Important -Message "//================= Generate package ==========================================//" switch ($settings.namingStrategy) { { $settings.namingStrategy -eq "Default" } { $packageNamePattern = $settings.packageNamePattern; if($settings.packageName.Contains('.zip')) { $packageName = $settings.packageName } else { $packageName = $settings.packageName# + ".zip" } $packageNamePattern = $packageNamePattern.Replace("BRANCHNAME", $($settings.sourceBranch)) if($settings.deploy) { $packageNamePattern = $packageNamePattern.Replace("PACKAGENAME", $settings.azVMName) } else { $packageNamePattern = $packageNamePattern.Replace("PACKAGENAME", $packageName) } $packageNamePattern = $packageNamePattern.Replace("FNSCMVERSION", $Version) $packageNamePattern = $packageNamePattern.Replace("DATE", (Get-Date -Format "yyyyMMdd").ToString()) $packageNamePattern = $packageNamePattern.Replace("RUNNUMBER", $settings.runId) $packageName = $packageNamePattern + ".zip" break; } { $settings.namingStrategy -eq "Custom" } { if($settings.packageName.Contains('.zip')) { $packageName = $settings.packageName } else { $packageName = $settings.packageName + ".zip" } break; } Default { $packageName = $settings.packageName break; } } $xppToolsPath = $msFrameworkDirectory $xppBinariesPath = (Join-Path $($BuildFolderPath) bin) $xppBinariesSearch = $modelsToPackage $deployablePackagePath = Join-Path $artifactDirectory ($packageName) if ($xppBinariesSearch.Contains(",")) { [string[]]$xppBinariesSearch = $xppBinariesSearch -split "," } $potentialPackages = Find-FSCPSMatch -DefaultRoot $xppBinariesPath -Pattern $xppBinariesSearch | Where-Object { (Test-Path -LiteralPath $_ -PathType Container) } $packages = @() if ($potentialPackages.Length -gt 0) { Write-PSFMessage -Level Verbose -Message "Found $($potentialPackages.Length) potential folders to include:" foreach($package in $potentialPackages) { $packageBinPath = Join-Path -Path $package -ChildPath "bin" # If there is a bin folder and it contains *.MD files, assume it's a valid X++ binary try { if ((Test-Path -Path $packageBinPath) -and ((Get-ChildItem -Path $packageBinPath -Filter *.md).Count -gt 0)) { Write-PSFMessage -Level Verbose -Message $packageBinPath Write-PSFMessage -Level Verbose -Message " - $package" $packages += $package } } catch { Write-PSFMessage -Level Verbose -Message " - $package (not an X++ binary folder, skip)" } } Import-Module (Join-Path -Path $xppToolsPath -ChildPath "CreatePackage.psm1") $outputDir = Join-Path -Path $BuildFolderPath -ChildPath ((New-Guid).ToString()) $tempCombinedPackage = Join-Path -Path $BuildFolderPath -ChildPath "$((New-Guid).ToString()).zip" try { New-Item -Path $outputDir -ItemType Directory > $null Write-PSFMessage -Level Verbose -Message "Creating binary packages" Invoke-FSCAssembliesImport $xppToolsPath -Verbose foreach($packagePath in $packages) { $packageName = (Get-Item $packagePath).Name Write-PSFMessage -Level Verbose -Message " - '$packageName'" $version = "" $packageDll = Join-Path -Path $packagePath -ChildPath "bin\Dynamics.AX.$packageName.dll" if (Test-Path $packageDll) { $version = (Get-Item $packageDll).VersionInfo.FileVersion } if (!$version) { $version = "1.0.0.0" } $null = New-XppRuntimePackage -packageName $packageName -packageDrop $packagePath -outputDir $outputDir -metadataDir $xppBinariesPath -packageVersion $version -binDir $xppToolsPath -enforceVersionCheck $True } Write-PSFMessage -Level Important "Creating deployable package" Add-Type -Path "$xppToolsPath\Microsoft.Dynamics.AXCreateDeployablePackageBase.dll" Write-PSFMessage -Level Important " - Creating combined metadata package" $null = [Microsoft.Dynamics.AXCreateDeployablePackageBase.BuildDeployablePackages]::CreateMetadataPackage($outputDir, $tempCombinedPackage) Write-PSFMessage -Level Important " - Creating merged deployable package" $null = [Microsoft.Dynamics.AXCreateDeployablePackageBase.BuildDeployablePackages]::MergePackage("$xppToolsPath\BaseMetadataDeployablePackage.zip", $tempCombinedPackage, $deployablePackagePath, $true, [String]::Empty) Write-PSFMessage -Level Important "Deployable package '$deployablePackagePath' successfully created." $pname = ($deployablePackagePath.SubString("$deployablePackagePath".LastIndexOf('\') + 1)).Replace(".zip","") $responseObject.PACKAGE_NAME = $pname $responseObject.PACKAGE_PATH = $deployablePackagePath $responseObject.ARTIFACTS_PATH = $artifactDirectory } catch { throw $_.Exception.Message } } else { throw "No X++ binary package(s) found" } Write-PSFMessage -Level Important -Message "Complete" } } if($settings.exportModel) { Write-PSFMessage -Level Important -Message "//================= Export models ===========================================//" try { $axModelFolder = Join-Path $artifactDirectory AxModels $null = Test-PathExists -Path $axModelFolder -Type Container -Create Write-PSFMessage -Level Verbose -Message "$axModelFolder created" if($models.Split(",")) { $modelsList = $models.Split(",") foreach ($currentModel in $modelsList) { Write-PSFMessage -Level Verbose -Message "Exporting $currentModel model..." $modelName = (Get-AXModelName -ModelName $currentModel -ModelPath $msMetadataDirectory) if($modelName) { $modelFilePath = Export-D365Model -Path $axModelFolder -Model $modelName -BinDir $msFrameworkDirectory -MetaDataDir $msMetadataDirectory -ShowOriginalProgress $modelFile = Get-Item $modelFilePath.File Rename-Item $modelFile.FullName (($currentModel)+($modelFile.Extension)) -Force } else { Write-PSFMessage -Level Verbose -Message "The model $modelName doesn`t have the source code. Skipped." } } } else { Write-PSFMessage -Level Verbose -Message "Exporting $models model..." $modelName = (Get-AXModelName -ModelName $models -ModelPath $msMetadataDirectory) if($modelName) { $modelFilePath = Export-D365Model -Path $axModelFolder -Model $modelName -BinDir $msFrameworkDirectory -MetaDataDir $msMetadataDirectory $modelFile = Get-Item $modelFilePath.File Rename-Item $modelFile.FullName (($models)+($modelFile.Extension)) -Force } else { Write-PSFMessage -Level Verbose -Message "The model $models doesn`t have the source code. Skipped." } } } catch { Write-PSFMessage -Level Important -Message $_.Exception.Message } Write-PSFMessage -Level Important -Message "Complete" } $artifacts = Get-ChildItem $artifactDirectory -File -Recurse $artifactsList = $artifacts.FullName -join "," if($artifactsList.Contains(',')) { $artifacts = $artifactsList.Split(',') | ConvertTo-Json -compress } else { $artifacts = '["'+$($artifactsList).ToString()+'"]' } $responseObject.ARTIFACTS_LIST = $artifacts } catch { Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } finally { try { if($SolutionBuildFolderPath) { if (Test-Path -Path $SolutionBuildFolderPath -ErrorAction SilentlyContinue) { Remove-Item -Path $SolutionBuildFolderPath -Recurse -Force -ErrorAction SilentlyContinue } } if($NuGetPackagesPath) { if (Test-Path -Path $NuGetPackagesPath -ErrorAction SilentlyContinue) { Remove-Item -Path $NuGetPackagesPath -Recurse -Force -ErrorAction SilentlyContinue } } if($outputDir) { if (Test-Path -Path $outputDir -ErrorAction SilentlyContinue) { Remove-Item -Path $outputDir -Recurse -Force -ErrorAction SilentlyContinue } } if($tempCombinedPackage) { if (Test-Path -Path $tempCombinedPackage -ErrorAction SilentlyContinue) { Remove-Item -Path $tempCombinedPackage -Recurse -Force -ErrorAction SilentlyContinue } } } catch { Write-PSFMessage -Level Verbose -Message "Cleanup warning: $($PSItem.Exception)" } $responseObject } } END { Invoke-TimeSignal -End } } <# .SYNOPSIS HTTP request wrapper .DESCRIPTION HTTP request wrapper .PARAMETER headers HTTP request headers parameter .PARAMETER method HTTP request method parameter .PARAMETER body HTTP request body parameter .PARAMETER outFile HTTP outfile parameter .PARAMETER uri Parameter description .EXAMPLE PS C:\> Invoke-FSCPSWebRequest -Uri "google.com" This will invoke google.com .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-FSCPSWebRequest { Param( [Hashtable] $headers, [string] $method, [string] $body, [string] $outFile, [string] $uri ) try { $params = @{ "UseBasicParsing" = $true } if ($headers) { $params += @{ "headers" = $headers } } if ($method) { $params += @{ "method" = $method } } if ($body) { $params += @{ "body" = $body } } if ($outfile) { if(-not (Test-Path $outFile)) { $null = New-Item -Path $outFile -Force } $params += @{ "outfile" = $outfile } } Invoke-WebRequest @params -Uri $uri } catch { $errorRecord = $_ $exception = $_.Exception $message = $exception.Message try { if ($errorRecord.ErrorDetails) { $errorDetails = $errorRecord.ErrorDetails | ConvertFrom-Json $errorDetails.psObject.Properties.name | ForEach-Object { $message += " $($errorDetails."$_")" } } } catch { Write-PSFMessage -Level Host -Message "Error occured" } throw $message } } <# .SYNOPSIS Generate the D365FSC build solution .DESCRIPTION Invoke the D365FSC generation build solution .PARAMETER ModelsList The list of models to generate a solution .PARAMETER DynamicsVersion The version of the D365FSC to build .PARAMETER MetadataPath The path to the metadata folder .PARAMETER SolutionBasePath The path to the generated solution folder. Dafault is c:\temp\fscps.tools\ .EXAMPLE PS C:\> Invoke-GenerateSolution -Models "Test, SuperTest, SuperTestExtension" -Version "10.0.39" -MetadataPath "c:\temp\TestMetadataFolder" This will generate a solution of 10.0.39 version .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-GenerateSolution { [CmdletBinding()] param ( [Parameter(Mandatory = $false)] [Alias('Models')] [string]$ModelsList, [Parameter(Mandatory = $true)] [Alias('Version')] [string]$DynamicsVersion, [Parameter(Mandatory = $true)] [string]$MetadataPath, [Alias('SolutionFolderPath')] [string]$SolutionBasePath = $script:DefaultTempPath ) BEGIN { $miscFolder = (Join-Path $script:ModuleRoot "\internal\misc") $buildSolutionTemplateFolder = (Join-Path $miscFolder \Build) $buildProjectTemplateFolder = (Join-Path $buildSolutionTemplateFolder \Build) #Set-Location $buildProjectTemplateFolder Write-PSFMessage -Level Debug -Message "MetadataPath: $MetadataPath" $ProjectPattern = 'Project("{FC65038C-1B2F-41E1-A629-BED71D161FFF}") = "ModelNameBuild (ISV) [ModelDisplayName]", "ModelName.rnrproj", "{62C69717-A1B6-43B5-9E86-24806782FEC2}"' $ActiveCFGPattern = ' {62C69717-A1B6-43B5-9E86-24806782FEC2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU' $BuildPattern = ' {62C69717-A1B6-43B5-9E86-24806782FEC2}.Debug|Any CPU.Build.0 = Debug|Any CPU' $SolutionFileName = 'Build.sln' $NugetFolderPath = Join-Path $SolutionBasePath "$($DynamicsVersion)_build" $SolutionFolderPath = Join-Path $NugetFolderPath 'Build' $NewSolutionName = Join-Path $SolutionFolderPath 'Build.sln' function Get-AXModelDisplayName { param ( [Alias('ModelName')] [string]$_modelName, [Alias('ModelPath')] [string]$_modelPath ) process{ $descriptorSearchPath = (Join-Path $_modelPath (Join-Path $_modelName "Descriptor")) $descriptor = (Get-ChildItem -Path $descriptorSearchPath -Filter '*.xml') if($descriptor) { Write-PSFMessage -Level Verbose -Message "Descriptor found at $descriptor" [xml]$xmlData = Get-Content $descriptor.FullName $modelDisplayName = $xmlData.SelectNodes("//AxModelInfo/DisplayName") return $modelDisplayName.InnerText } } } function GenerateProjectFile { [CmdletBinding()] param ( [string]$ModelName, [string]$MetadataPath, [string]$ProjectGuid ) $ProjectFileName = 'Build.rnrproj' $ModelProjectFileName = $ModelName + '.rnrproj' $NugetFolderPath = Join-Path $SolutionBasePath "$($DynamicsVersion)_build" $SolutionFolderPath = Join-Path $NugetFolderPath 'Build' $ModelProjectFile = Join-Path $SolutionFolderPath $ModelProjectFileName #$modelDisplayName = Get-AXModelDisplayName -ModelName $ModelName -ModelPath $MetadataPath $modelDescriptorName = Get-AXModelName -ModelName $ModelName -ModelPath $MetadataPath #generate project file if($modelDescriptorName -eq "") { $ProjectFileData = (Get-Content $buildProjectTemplateFolder\$ProjectFileName).Replace('ModelName', $ModelName).Replace('62C69717-A1B6-43B5-9E86-24806782FEC2'.ToLower(), $ProjectGuid.ToLower()) } else { $ProjectFileData = (Get-Content $buildProjectTemplateFolder\$ProjectFileName).Replace('ModelName', $modelDescriptorName).Replace('62C69717-A1B6-43B5-9E86-24806782FEC2'.ToLower(), $ProjectGuid.ToLower()) } #$ProjectFileData = (Get-Content $ProjectFileName).Replace('ModelName', $modelDescriptorName).Replace('62C69717-A1B6-43B5-9E86-24806782FEC2'.ToLower(), $ProjectGuid.ToLower()) Set-Content $ModelProjectFile $ProjectFileData } } PROCESS { New-Item -ItemType Directory -Path $SolutionFolderPath -ErrorAction SilentlyContinue Copy-Item $buildProjectTemplateFolder\build.props -Destination $SolutionFolderPath -force [String[]] $SolutionFileData = @() $projectGuids = @{}; Write-PSFMessage -Level Debug -Message "Generate projects GUIDs..." if($ModelsList) { Foreach($model in $ModelsList.Split(',')) { $projectGuids.Add($model, ([string][guid]::NewGuid()).ToUpper()) } Write-PSFMessage -Level Debug -Message $projectGuids #generate project files file $FileOriginal = Get-Content $buildProjectTemplateFolder\$SolutionFileName Write-PSFMessage -Level Debug -Message "Parse files" Foreach ($Line in $FileOriginal) { $SolutionFileData += $Line Foreach($model in $ModelsList.Split(',')) { $projectGuid = $projectGuids.Item($model) if ($Line -eq $ProjectPattern) { Write-PSFMessage -Level Debug -Message "Get AXModel Display Name" $modelDisplayName = Get-AXModelDisplayName -ModelName $model -ModelPath $MetadataPath Write-PSFMessage -Level Debug -Message "AXModel Display Name is $modelDisplayName" Write-PSFMessage -Level Debug -Message "Update Project line" $newLine = $ProjectPattern -replace 'ModelName', $model $newLine = $newLine -replace 'ModelDisplayName', $modelDisplayName $newLine = $newLine -replace 'Build.rnrproj', ($model+'.rnrproj') $newLine = $newLine -replace '62C69717-A1B6-43B5-9E86-24806782FEC2', $projectGuid #Add Lines after the selected pattern $SolutionFileData += $newLine $SolutionFileData += "EndProject" } if ($Line -eq $ActiveCFGPattern) { Write-PSFMessage -Level Debug -Message "Update Active CFG line" $newLine = $ActiveCFGPattern -replace '62C69717-A1B6-43B5-9E86-24806782FEC2', $projectGuid $SolutionFileData += $newLine } if ($Line -eq $BuildPattern) { Write-PSFMessage -Level Debug -Message "Update Build line" $newLine = $BuildPattern -replace '62C69717-A1B6-43B5-9E86-24806782FEC2', $projectGuid $SolutionFileData += $newLine } } } Write-PSFMessage -Level Debug -Message "Save solution file" #save solution file Set-Content $NewSolutionName $SolutionFileData; #cleanup solution file $tempFile = Get-Content $NewSolutionName $tempFile | Where-Object {$_ -ne $ProjectPattern} | Where-Object {$_ -ne $ActiveCFGPattern} | Where-Object {$_ -ne $BuildPattern} | Set-Content -Path $NewSolutionName #generate project files Foreach($project in $projectGuids.GetEnumerator()) { GenerateProjectFile -ModelName $project.Name -ProjectGuid $project.Value -MetadataPath $MetadataPath } #Set-Location $buildSolutionTemplateFolder } #generate nuget.config $NugetConfigFileName = 'nuget.config' $NewNugetFile = Join-Path $NugetFolderPath $NugetConfigFileName if($NugetFeedName) { $tempFile = (Get-Content $buildSolutionTemplateFolder\$NugetConfigFileName).Replace('NugetFeedName', $NugetFeedName).Replace('NugetSourcePath', $NugetSourcePath) } else { $tempFile = (Get-Content $buildSolutionTemplateFolder\$NugetConfigFileName).Replace('<add key="NugetFeedName" value="NugetSourcePath" />', '') } Set-Content $NewNugetFile $tempFile $version = Get-FSCPSVersionInfo -Version "$DynamicsVersion" #generate packages.config $PackagesConfigFileName = 'packages.config' $NewPackagesFile = Join-Path $NugetFolderPath $PackagesConfigFileName $tempFile = (Get-Content $buildSolutionTemplateFolder\$PackagesConfigFileName).Replace('PlatformVersion', $version.data.PlatformVersion).Replace('ApplicationVersion', $version.data.AppVersion) Set-Content $NewPackagesFile $tempFile } END{ } } <# .SYNOPSIS Invoke the ModelUtil.exe .DESCRIPTION A cmdlet that wraps some of the cumbersome work into a streamlined process .PARAMETER Command Instruct the cmdlet to what process you want to execute against the ModelUtil tool Valid options: Import Export Delete Replace .PARAMETER Path Used for import to point where to import from Used for export to point where to export the model to The cmdlet only supports an already extracted ".axmodel" file .PARAMETER Model Name of the model that you want to work against Used for export to select the model that you want to export Used for delete to select the model that you want to delete .PARAMETER BinDir The path to the bin directory for the environment Default path is the same as the AOS service PackagesLocalDirectory\bin Default value is fetched from the current configuration on the machine .PARAMETER MetaDataDir The path to the meta data directory for the environment Default path is the same as the aos service PackagesLocalDirectory .PARAMETER LogPath The path where the log file(s) will be saved .PARAMETER ShowOriginalProgress Instruct the cmdlet to show the standard output in the console Default is $false which will silence the standard output .PARAMETER OutputCommandOnly Instruct the cmdlet to only output the command that you would have to execute by hand Will include full path to the executable and the needed parameters based on your selection .EXAMPLE PS C:\> Invoke-ModelUtil -Command Import -Path "c:\temp\d365fo.tools\CustomModel.axmodel" This will execute the import functionality of ModelUtil.exe and have it import the "CustomModel.axmodel" file. .EXAMPLE PS C:\> Invoke-ModelUtil -Command Export -Path "c:\temp\d365fo.tools" -Model CustomModel This will execute the export functionality of ModelUtil.exe and have it export the "CustomModel" model. The file will be placed in "c:\temp\d365fo.tools". .EXAMPLE PS C:\> Invoke-ModelUtil -Command Delete -Model CustomModel This will execute the delete functionality of ModelUtil.exe and have it delete the "CustomModel" model. The folders in PackagesLocalDirectory for the "CustomModel" will NOT be deleted .EXAMPLE PS C:\> Invoke-ModelUtil -Command Replace -Path "c:\temp\d365fo.tools\CustomModel.axmodel" This will execute the replace functionality of ModelUtil.exe and have it replace the "CustomModel" model. .NOTES Tags: AXModel, Model, ModelUtil, Servicing, Import, Export, Delete, Replace This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-ModelUtil { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidDefaultValueSwitchParameter", "")] [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $true)] [ValidateSet('Import', 'Export', 'Delete', 'Replace')] [string] $Command, [Parameter(Mandatory = $True, ParameterSetName = 'Import', Position = 1 )] [Parameter(Mandatory = $True, ParameterSetName = 'Export', Position = 1 )] [Alias('File')] [string] $Path, [Parameter(Mandatory = $True, ParameterSetName = 'Export', Position = 2 )] [Parameter(Mandatory = $True, ParameterSetName = 'Delete', Position = 1 )] [string] $Model, [string] $BinDir = "$Script:PackageDirectory\bin", [string] $MetaDataDir = "$Script:MetaDataDir", [string] $LogPath, [switch] $ShowOriginalProgress, [switch] $OutputCommandOnly ) Invoke-TimeSignal -Start if (-not (Test-PathExists -Path $MetaDataDir, $BinDir -Type Container)) { Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 } $executable = Join-Path -Path $BinDir -ChildPath "ModelUtil.exe" if (-not (Test-PathExists -Path $executable -Type Leaf)) { Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 } $params = New-Object System.Collections.Generic.List[string] Write-PSFMessage -Level Verbose -Message "Building the parameter options." switch ($Command.ToLowerInvariant()) { 'import' { if (-not (Test-PathExists -Path $Path -Type Leaf)) { Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 } $params.Add("-import") $params.Add("-metadatastorepath=`"$MetaDataDir`"") $params.Add("-file=`"$Path`"") } 'export' { $params.Add("-export") $params.Add("-metadatastorepath=`"$MetaDataDir`"") $params.Add("-outputpath=`"$Path`"") $params.Add("-modelname=`"$Model`"") } 'delete' { $params.Add("-delete") $params.Add("-metadatastorepath=`"$MetaDataDir`"") $params.Add("-modelname=`"$Model`"") } 'replace' { if (-not (Test-PathExists -Path $Path -Type Leaf)) { Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 } $params.Add("-replace") $params.Add("-metadatastorepath=`"$MetaDataDir`"") $params.Add("-file=`"$Path`"") $params.Add("-force") } } Write-PSFMessage -Level Verbose -Message "Starting the $executable with the parameter options." -Target $($params.ToArray() -join " ") Invoke-Process -Executable $executable -Params $params.ToArray() -ShowOriginalProgress:$ShowOriginalProgress -OutputCommandOnly:$OutputCommandOnly -LogPath $LogPath if (Test-PSFFunctionInterrupt) { Stop-PSFFunction -Message "Stopping because of 'ModelUtil.exe' failed its execution." -StepsUpward 1 return } Invoke-TimeSignal -End } <# .SYNOPSIS Invoke a process .DESCRIPTION Invoke a process and pass the needed parameters to it .PARAMETER Path Path to the program / executable that you want to start .PARAMETER Params Array of string parameters that you want to pass to the executable .PARAMETER LogPath The path where the log file(s) will be saved .PARAMETER ShowOriginalProgress Instruct the cmdlet to show the standard output in the console Default is $false which will silence the standard output .PARAMETER OutputCommandOnly Instruct the cmdlet to only output the command that you would have to execute by hand Will include full path to the executable and the needed parameters based on your selection .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions This is less user friendly, but allows catching exceptions in calling scripts .EXAMPLE PS C:\> Invoke-Process -Path "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" -Params "-metadata=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-modelmodule=`"ApplicationSuite`"", "-output=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-referencefolder=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-log=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.$Module.xppc.log`"", "-xmlLog=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.ApplicationSuite.xppc.xml`"", "-verbose" This will invoke the "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" executable. All parameters will be passed to it. The standard output will be redirected to a local variable. The error output will be redirected to a local variable. The standard output will be written to the verbose stream before exiting. If an error should occur, both the standard output and error output will be written to the console / host. .EXAMPLE PS C:\> Invoke-Process -ShowOriginalProgress -Path "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" -Params "-metadata=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-modelmodule=`"ApplicationSuite`"", "-output=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-referencefolder=`"C:\AOSService\PackagesLocalDirectory\Bin`"", "-log=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.$Module.xppc.log`"", "-xmlLog=`"C:\temp\d365fo.tools\ApplicationSuite\Dynamics.AX.ApplicationSuite.xppc.xml`"", "-verbose" This will invoke the "C:\AOSService\PackagesLocalDirectory\Bin\xppc.exe" executable. All parameters will be passed to it. The standard output will be outputted directly to the console / host. The error output will be outputted directly to the console / host. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-Process { [CmdletBinding()] [OutputType()] param ( [Parameter(Mandatory = $true)] [Alias('Executable')] [string] $Path, [Parameter(Mandatory = $true)] [string[]] $Params, [Parameter(Mandatory = $false)] [string] $LogPath, [switch] $ShowOriginalProgress, [switch] $OutputCommandOnly, [switch] $EnableException ) Invoke-TimeSignal -Start if (-not (Test-PathExists -Path $Path -Type Leaf)) { return } if (Test-PSFFunctionInterrupt) { return } $tool = Split-Path -Path $Path -Leaf $pinfo = New-Object System.Diagnostics.ProcessStartInfo $pinfo.FileName = "$Path" $pinfo.WorkingDirectory = Split-Path -Path $Path -Parent if (-not $ShowOriginalProgress) { Write-PSFMessage -Level Verbose "Output and Error streams will be redirected (silence mode)" $pinfo.RedirectStandardError = $true $pinfo.RedirectStandardOutput = $true } $pinfo.UseShellExecute = $false $pinfo.Arguments = "$($Params -join " ")" $p = New-Object System.Diagnostics.Process $p.StartInfo = $pinfo Write-PSFMessage -Level Verbose "Starting the $tool" -Target "$($params -join " ")" if ($OutputCommandOnly) { Write-PSFMessage -Level Host "$Path $($pinfo.Arguments)" return } $p.Start() | Out-Null if (-not $ShowOriginalProgress) { $outTask = $p.StandardOutput.ReadToEndAsync(); $errTask = $p.StandardError.ReadToEndAsync(); } Write-PSFMessage -Level Verbose "Waiting for the $tool to complete" $p.WaitForExit() if (-not $ShowOriginalProgress) { $stdout = $outTask.Result $stderr = $errTask.Result } if ($p.ExitCode -ne 0 -and (-not $ShowOriginalProgress)) { Write-PSFMessage -Level Host "Exit code from $tool indicated an error happened. Will output both standard stream and error stream." Write-PSFMessage -Level Host "Standard output was: \r\n $stdout" Write-PSFMessage -Level Host "Error output was: \r\n $stderr" $messageString = "Stopping because an Exit Code from $tool wasn't 0 (zero) like expected." Stop-PSFFunction -Message "Stopping because of Exit Code." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -StepsUpward 1 return } else { Write-PSFMessage -Level Verbose "Standard output was: \r\n $stdout" } if ((-not $ShowOriginalProgress) -and (-not ([string]::IsNullOrEmpty($LogPath)))) { if (-not (Test-PathExists -Path $LogPath -Type Container -Create)) { return } $stdOutputPath = Join-Path -Path $LogPath -ChildPath "$tool`_StdOutput.log" $errOutputPath = Join-Path -Path $LogPath -ChildPath "$tool`_ErrOutput.log" $stdout | Out-File -FilePath $stdOutputPath -Encoding utf8 -Force $stderr | Out-File -FilePath $errOutputPath -Encoding utf8 -Force } Invoke-TimeSignal -End } <# .SYNOPSIS Handle time measurement .DESCRIPTION Handle time measurement from when a cmdlet / function starts and ends Will write the output to the verbose stream (Write-PSFMessage -Level Verbose) .PARAMETER Start Switch to instruct the cmdlet that a start time registration needs to take place .PARAMETER End Switch to instruct the cmdlet that a time registration has come to its end and it needs to do the calculation .EXAMPLE PS C:\> Invoke-TimeSignal -Start This will start the time measurement for any given cmdlet / function .EXAMPLE PS C:\> Invoke-TimeSignal -End This will end the time measurement for any given cmdlet / function. The output will go into the verbose stream. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-TimeSignal { [CmdletBinding(DefaultParameterSetName = 'Start')] param ( [Parameter(Mandatory = $true, ParameterSetName = 'Start', Position = 1 )] [switch] $Start, [Parameter(Mandatory = $True, ParameterSetName = 'End', Position = 2 )] [switch] $End ) $Time = (Get-Date) $Command = (Get-PSCallStack)[1].Command if ($Start) { if ($Script:TimeSignals.ContainsKey($Command)) { Write-PSFMessage -Level Verbose -Message "The command '$Command' was already taking part in time measurement. The entry has been update with current date and time." $Script:TimeSignals[$Command] = $Time } else { $Script:TimeSignals.Add($Command, $Time) } } else { if ($Script:TimeSignals.ContainsKey($Command)) { $TimeSpan = New-TimeSpan -End $Time -Start (($Script:TimeSignals)[$Command]) Write-PSFMessage -Level Verbose -Message "Total time spent inside the function was $TimeSpan" -Target $TimeSpan -FunctionName $Command -Tag "TimeSignal" $null = $Script:TimeSignals.Remove($Command) } else { Write-PSFMessage -Level Verbose -Message "The command '$Command' was never started to take part in time measurement." } } } <# .SYNOPSIS Short description .DESCRIPTION Long description .PARAMETER InputObject Parameter description .PARAMETER Property Parameter description .PARAMETER ExcludeProperty Parameter description .PARAMETER TypeName Parameter description .EXAMPLE PS C:\> Select-DefaultView -InputObject $result -Property CommandName, Synopsis This will help you do it right. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Select-DefaultView { <# This command enables us to send full on objects to the pipeline without the user seeing it a lot of this is from boe, thanks boe! https://learn-powershell.net/2013/08/03/quick-hits-set-the-default-property-display-in-powershell-on-custom-objects/ TypeName creates a new type so that we can use ps1xml to modify the output #> [CmdletBinding()] param ( [parameter(ValueFromPipeline)] [object] $InputObject, [string[]] $Property, [string[]] $ExcludeProperty, [string] $TypeName ) process { if ($null -eq $InputObject) { return } if ($TypeName) { $InputObject.PSObject.TypeNames.Insert(0, "fscps.tools.$TypeName") } if ($ExcludeProperty) { if ($InputObject.GetType().Name.ToString() -eq 'DataRow') { $ExcludeProperty += 'Item', 'RowError', 'RowState', 'Table', 'ItemArray', 'HasErrors' } $props = ($InputObject | Get-Member | Where-Object MemberType -in 'Property', 'NoteProperty', 'AliasProperty' | Where-Object { $_.Name -notin $ExcludeProperty }).Name $defaultset = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$props) } else { # property needs to be string if ("$property" -like "* as *") { $newproperty = @() foreach ($p in $property) { if ($p -like "* as *") { $old, $new = $p -isplit " as " # Do not be tempted to not pipe here $inputobject | Add-Member -Force -MemberType AliasProperty -Name $new -Value $old -ErrorAction SilentlyContinue $newproperty += $new } else { $newproperty += $p } } $property = $newproperty } $defaultset = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet', [string[]]$Property) } $standardmembers = [System.Management.Automation.PSMemberInfo[]]@($defaultset) # Do not be tempted to not pipe here $inputobject | Add-Member -Force -MemberType MemberSet -Name PSStandardMembers -Value $standardmembers -ErrorAction SilentlyContinue $inputobject } } <# .SYNOPSIS Test accessible to the configuration storage .DESCRIPTION Test if the desired configuration storage is accessible with the current user context .PARAMETER ConfigStorageLocation Parameter used to instruct where to store the configuration objects The default value is "User" and this will store all configuration for the active user Valid options are: "User" "System" "System" will store the configuration so all users can access the configuration objects .EXAMPLE PS C:\> Test-ConfigStorageLocation -ConfigStorageLocation "System" This will test if the current executing user has enough privileges to save to the system wide configuration storage. The system wide configuration storage requires administrator rights. .NOTES Author: Mötz Jensen (@Splaxi) #> function Test-ConfigStorageLocation { [CmdletBinding()] [OutputType('System.String')] param ( [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User" ) $configScope = "UserDefault" if ($ConfigStorageLocation -eq "System") { if ($Script:IsAdminRuntime) { $configScope = "SystemDefault" } else { Write-PSFMessage -Level Host -Message "Unable to locate save the <c='em'>configuration objects</c> in the <c='em'>system wide configuration store</c> on the machine. Please start an elevated session and run the cmdlet again." Stop-PSFFunction -Message "Elevated permissions needed. Please start an elevated session and run the cmdlet again." -StepsUpward 1 return } } $configScope } <# .SYNOPSIS Test multiple paths .DESCRIPTION Easy way to test multiple paths for public functions and have the same error handling .PARAMETER Path Array of paths you want to test They have to be the same type, either file/leaf or folder/container .PARAMETER Type Type of path you want to test Either 'Leaf' or 'Container' .PARAMETER Create Instruct the cmdlet to create the directory if it doesn't exist .PARAMETER ShouldNotExist Instruct the cmdlet to return true if the file doesn't exists .EXAMPLE PS C:\> Test-PathExists "c:\temp","c:\temp\dir" -Type Container This will test if the mentioned paths (folders) exists and the current context has enough permission. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Test-PathExists { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $True)] [AllowEmptyString()] [string[]] $Path, [ValidateSet('Leaf', 'Container')] [Parameter(Mandatory = $True)] [string] $Type, [switch] $Create, [switch] $ShouldNotExist ) $res = $false $arrList = New-Object -TypeName "System.Collections.ArrayList" foreach ($item in $Path) { if ([string]::IsNullOrEmpty($item)) { Stop-PSFFunction -Message "Stopping because path was either null or empty string." -StepsUpward 1 return } Write-PSFMessage -Level Debug -Message "Testing the path: $item" -Target $item $temp = Test-Path -Path $item -Type $Type if ((-not $temp) -and ($Create) -and ($Type -eq "Container")) { Write-PSFMessage -Level Debug -Message "Creating the path: $item" -Target $item $null = New-Item -Path $item -ItemType Directory -Force -ErrorAction Stop $temp = $true } elseif ($ShouldNotExist) { Write-PSFMessage -Level Debug -Message "The should NOT exists: $item" -Target $item } elseif ((-not $temp) -and ($WarningPreference -ne [System.Management.Automation.ActionPreference]::SilentlyContinue)) { Write-PSFMessage -Level Host -Message "The <c='em'>$item</c> path wasn't found. Please ensure the path <c='em'>exists</c> and you have enough <c='em'>permission</c> to access the path." } $null = $arrList.Add($temp) } if ($arrList.Contains($false) -and (-not $ShouldNotExist)) { # The $ErrorActionPreference variable determines the behavior we are after, but the "Stop-PSFFunction -WarningAction" is where we need to put in the value. Stop-PSFFunction -Message "Stopping because of missing paths." -StepsUpward 1 -WarningAction $ErrorActionPreference } elseif ($arrList.Contains($true) -and $ShouldNotExist) { # The $ErrorActionPreference variable determines the behavior we are after, but the "Stop-PSFFunction -WarningAction" is where we need to put in the value. Stop-PSFFunction -Message "Stopping because file exists." -StepsUpward 1 -WarningAction $ErrorActionPreference } else { $res = $true } $res } <# .SYNOPSIS Test if a given registry key exists or not .DESCRIPTION Test if a given registry key exists in the path specified .PARAMETER Path Path to the registry hive and sub directories you want to work against .PARAMETER Name Name of the registry key that you want to test for .EXAMPLE PS C:\> Test-RegistryValue -Path "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment\" -Name "InstallationInfoDirectory" This will query the LocalMachine hive and the sub directories "HKLM:\SOFTWARE\Microsoft\Dynamics\Deployment\" for a registry key with the name of "InstallationInfoDirectory". .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> Function Test-RegistryValue { [OutputType('System.Boolean')] param( [Parameter(Mandatory = $true)] [string]$Path, [Parameter(Mandatory = $true)] [string]$Name ) if (Test-Path -Path $Path -PathType Any) { $null -ne (Get-ItemProperty $Path).$Name } else { $false } } <# .SYNOPSIS Update the Azure Storage config variables .DESCRIPTION Update the active Azure Storage config variables that the module will use as default values .EXAMPLE PS C:\> Update-AzureStorageVariables This will update the Azure Storage variables. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Update-AzureStorageVariables { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType()] param ( ) $hashParameters = Get-FSCPSActiveAzureStorageConfig foreach ($item in $hashParameters.Keys) { $name = "AzureStorage" + (Get-Culture).TextInfo.ToTitleCase($item) Write-PSFMessage -Level Verbose -Message "$name - $($hashParameters[$item])" -Target $hashParameters[$item] Set-Variable -Name $name -Value $hashParameters[$item] -Scope Script -Force } } <# .SYNOPSIS Update the broadcast message config variables .DESCRIPTION Update the active broadcast message config variables that the module will use as default values .EXAMPLE PS C:\> Update-BroadcastVariables This will update the broadcast variables. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Update-BroadcastVariables { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType()] param ( ) $configName = (Get-PSFConfig -FullName "fscps.tools.active.broadcast.message.config.name").Value.ToString().ToLower() if (-not ($configName -eq "")) { $hashParameters = Get-FSCPSActiveBroadcastMessageConfig -OutputAsHashtable foreach ($item in $hashParameters.Keys) { if ($item -eq "name") { continue } $name = "Broadcast" + (Get-Culture).TextInfo.ToTitleCase($item) $valueMessage = $hashParameters[$item] if ($item -like "*client*" -and $valueMessage.Length -gt 20) { $valueMessage = $valueMessage.Substring(0,18) + "[...REDACTED...]" } Write-PSFMessage -Level Verbose -Message "$name - $valueMessage" -Target $valueMessage Set-Variable -Name $name -Value $hashParameters[$item] -Scope Script } } } <# .SYNOPSIS Update the LCS API config variables .DESCRIPTION Update the active LCS API config variables that the module will use as default values .EXAMPLE PS C:\> Update-LcsApiVariables This will update the LCS API variables. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Update-LcsApiVariables { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType()] param ( ) $hashParameters = Get-D365LcsApiConfig -OutputAsHashtable foreach ($item in $hashParameters.Keys) { $name = "LcsApi" + (Get-Culture).TextInfo.ToTitleCase($item) $valueMessage = $hashParameters[$item] if ($item -like "*client*" -and $valueMessage.Length -gt 20) { $valueMessage = $valueMessage.Substring(0,18) + "[...REDACTED...]" } Write-PSFMessage -Level Verbose -Message "$name - $valueMessage" -Target $valueMessage Set-Variable -Name $name -Value $hashParameters[$item] -Scope Script } } <# .SYNOPSIS Update module variables .DESCRIPTION Loads configuration variables again, to make sure things are updated based on changed configuration .EXAMPLE PS C:\> Update-ModuleVariables This will update internal variables that the module is dependent on. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Update-ModuleVariables { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [CmdletBinding()] [OutputType()] param ( ) Update-PsfConfigVariables $Script:AADOAuthEndpoint = Get-PSFConfigValue -FullName "fscps.tools.azure.common.oauth.token" } <# .SYNOPSIS Update the module variables based on the PSF Configuration store .DESCRIPTION Will read the current PSF Configuration store and create local module variables .EXAMPLE PS C:\> Update-PsfConfigVariables This will read all relevant PSF Configuration values and create matching module variables. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Update-PsfConfigVariables { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] [OutputType()] param () foreach ($config in Get-PSFConfig -FullName "fscps.tools.path.*") { $item = $config.FullName.Replace("fscps.tools.path.", "") $name = (Get-Culture).TextInfo.ToTitleCase($item) + "Path" Set-Variable -Name $name -Value $config.Value -Scope Script } } <# .SYNOPSIS Update the topology file .DESCRIPTION Update the topology file based on the already installed list of services on the machine .PARAMETER Path Path to the folder where the topology XML file that you want to work against is placed Should only contain a path to a folder, not a file .EXAMPLE PS C:\> Update-TopologyFile -Path "c:\temp\fscps.tools\DefaultTopologyData.xml" This will update the "c:\temp\fscps.tools\DefaultTopologyData.xml" file with all the installed services on the machine. .NOTES # Credit http://dev.goshoom.net/en/2016/11/installing-deployable-packages-with-powershell/ Author: Tommy Skaue (@Skaue) Author: Mötz Jensen (@Splaxi) #> function Update-TopologyFile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] [OutputType([System.Boolean])] param ( [Parameter(Mandatory = $true)] [string]$Path ) $topologyFile = Join-Path $Path 'DefaultTopologyData.xml' Write-PSFMessage -Level Verbose "Creating topology file: $topologyFile" [xml]$xml = Get-Content $topologyFile $machine = $xml.TopologyData.MachineList.Machine $machine.Name = $env:computername $serviceModelList = $machine.ServiceModelList $null = $serviceModelList.RemoveAll() [System.Collections.ArrayList] $Files2Process = New-Object -TypeName "System.Collections.ArrayList" $null = $Files2Process.Add((Join-Path $Path 'Microsoft.Dynamics.AX.AXInstallationInfo.dll')) Import-AssemblyFileIntoMemory -Path $($Files2Process.ToArray()) $models = [Microsoft.Dynamics.AX.AXInstallationInfo.AXInstallationInfo]::GetInstalledServiceModel() foreach ($name in $models.Name) { $element = $xml.CreateElement('string') $element.InnerText = $name $serviceModelList.AppendChild($element) } $xml.Save($topologyFile) $true } <# .SYNOPSIS Save an Azure Storage Account config .DESCRIPTION Adds an Azure Storage Account config to the configuration store .PARAMETER Name The logical name of the Azure Storage Account you are about to registered in the configuration store .PARAMETER AccountId The account id for the Azure Storage Account you want to register in the configuration store .PARAMETER AccessToken The access token for the Azure Storage Account you want to register in the configuration store .PARAMETER SAS The SAS key that you have created for the storage account or blob container .PARAMETER Container The name of the blob container inside the Azure Storage Account you want to register in the configuration store .PARAMETER Temporary Instruct the cmdlet to only temporarily add the azure storage account configuration in the configuration store .PARAMETER Force Switch to instruct the cmdlet to overwrite already registered Azure Storage Account entry .EXAMPLE PS C:\> Add-FSCPSAzureStorageConfig -Name "UAT-Exports" -AccountId "1234" -AccessToken "dafdfasdfasdf" -Container "testblob" This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", AccessToken "dafdfasdfasdf" and blob container "testblob". .EXAMPLE PS C:\> Add-FSCPSAzureStorageConfig -Name UAT-Exports -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -AccountId "1234" -Container "testblob" This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", SAS "sv=2018-03-28&si=unlisted&sr=c&sig=AUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" and blob container "testblob". The SAS key enables you to provide explicit access to a given blob container inside an Azure Storage Account. The SAS key can easily be revoked and that way you have control over the access to the container and its content. .EXAMPLE PS C:\> Add-FSCPSAzureStorageConfig -Name UAT-Exports -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -AccountId "1234" -Container "testblob" -Temporary This will add an entry into the list of Azure Storage Accounts that is stored with the name "UAT-Exports" with AccountId "1234", SAS "sv=2018-03-28&si=unlisted&sr=c&sig=AUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" and blob container "testblob". The SAS key enables you to provide explicit access to a given blob container inside an Azure Storage Account. The SAS key can easily be revoked and that way you have control over the access to the container and its content. The configuration will only last for the rest of this PowerShell console session. .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, Container This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Add-FSCPSAzureStorageConfig { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Name, [Parameter(Mandatory = $true)] [string] $AccountId, [Parameter(Mandatory = $true, ParameterSetName = "AccessToken")] [string] $AccessToken, [Parameter(Mandatory = $true, ParameterSetName = "SAS")] [string] $SAS, [Parameter(Mandatory = $true)] [Alias('Blob')] [Alias('Blobname')] [string] $Container, [switch] $Temporary, [switch] $Force ) $Details = @{AccountId = $AccountId.ToLower(); Container = $Container.ToLower(); } if ($PSCmdlet.ParameterSetName -eq "AccessToken") { $Details.AccessToken = $AccessToken } if ($PSCmdlet.ParameterSetName -eq "SAS") { if ($SAS.StartsWith("?")) { $SAS = $SAS.Substring(1) } $Details.SAS = $SAS } $Accounts = [hashtable](Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts") if(-not $Accounts) { $Accounts = @{} } if ($Accounts.ContainsKey($Name)) { if ($Force) { $Accounts[$Name] = $Details Set-PSFConfig -FullName "fscps.tools.azure.storage.accounts" -Value $Accounts } else { Write-PSFMessage -Level Host -Message "An Azure Storage Account with that name <c='em'>already exists</c>. If you want to <c='em'>overwrite</c> the already registered details please supply the <c='em'>-Force</c> parameter." Stop-PSFFunction -Message "Stopping because an Azure Storage Account already exists with that name." return } } else { $null = $Accounts.Add($Name, $Details) Set-PSFConfig -FullName "fscps.tools.azure.storage.accounts" -Value $Accounts } if (-not $Temporary) { Register-PSFConfig -FullName "fscps.tools.azure.storage.accounts" -Scope UserDefault } } <# .SYNOPSIS Disables throwing of exceptions .DESCRIPTION Restore the default exception behavior of the module to not support throwing exceptions Useful when the default behavior was changed with Enable-FSCPSException and the default behavior should be restored .EXAMPLE PS C:\>Disable-FSCPSException This will restore the default behavior of the module to not support throwing exceptions. .NOTES Tags: Exception, Exceptions, Warning, Warnings This is refactored function from d365fo.tools Original Author: Florian Hopfner (@FH-Inway) Author: Oleksandr Nikolaiev (@onikolaiev) .LINK Enable-FSCPSException #> function Disable-FSCPSException { [CmdletBinding()] param () Write-PSFMessage -Level Verbose -Message "Disabling exception across the entire module." -Target $configurationValue Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $false -ModuleName "fscps.tools" Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $false -ModuleName "PSOAuthHelper" $PSDefaultParameterValues['*:EnableException'] = $false } <# .SYNOPSIS Enable exceptions to be thrown .DESCRIPTION Change the default exception behavior of the module to support throwing exceptions Useful when the module is used in an automated fashion, like inside Azure DevOps pipelines and large PowerShell scripts .EXAMPLE PS C:\>Enable-FSCPSException This will for the rest of the current PowerShell session make sure that exceptions will be thrown. .NOTES Tags: Exception, Exceptions, Warning, Warnings This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) .LINK Disable-FSCPSException #> function Enable-FSCPSException { [CmdletBinding()] param () Write-PSFMessage -Level Verbose -Message "Enabling exception across the entire module." -Target $configurationValue Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $true -ModuleName "fscps.tools" Set-PSFFeature -Name 'PSFramework.InheritEnableException' -Value $true -ModuleName "PSOAuthHelper" $PSDefaultParameterValues['*:EnableException'] = $true } <# .SYNOPSIS Finds fscps.tools commands searching through the inline help text .DESCRIPTION Finds fscps.tools commands searching through the inline help text, building a consolidated json index and querying it because Get-Help is too slow .PARAMETER Tag Finds all commands tagged with this auto-populated tag .PARAMETER Author Finds all commands tagged with this author .PARAMETER MinimumVersion Finds all commands tagged with this auto-populated minimum version .PARAMETER MaximumVersion Finds all commands tagged with this auto-populated maximum version .PARAMETER Rebuild Rebuilds the index .PARAMETER Pattern Searches help for all commands in fscps.tools for the specified pattern and displays all results .PARAMETER Confirm Confirms overwrite of index .PARAMETER WhatIf Displays what would happen if the command is run .PARAMETER EnableException By default, when something goes wrong we try to catch it, interpret it and give you a friendly warning message. This avoids overwhelming you with "sea of red" exceptions, but is inconvenient because it basically disables advanced scripting. Using this switch turns this "nice by default" feature off and enables you to catch exceptions with your own try/catch. .EXAMPLE PS C:\> Find-FSCPSCommand "snapshot" For lazy typers: finds all commands searching the entire help for "snapshot" .EXAMPLE PS C:\> Find-FSCPSCommand -Pattern "snapshot" For rigorous typers: finds all commands searching the entire help for "snapshot" .EXAMPLE PS C:\> Find-FSCPSCommand -Tag copy Finds all commands tagged with "copy" .EXAMPLE PS C:\> Find-FSCPSCommand -Tag copy,user Finds all commands tagged with BOTH "copy" and "user" .EXAMPLE PS C:\> Find-FSCPSCommand -Author Mötz Finds every command whose author contains "Mötz" .EXAMPLE PS C:\> Find-FSCPSCommand -Author Mötz -Tag copy Finds every command whose author contains "Mötz" and it tagged as "copy" .EXAMPLE PS C:\> Find-FSCPSCommand -Rebuild Finds all commands and rebuilding the index (good for developers) .NOTES Tags: Find, Help, Command This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) License: MIT https://opensource.org/licenses/MIT This cmdlet / function is copy & paste implementation based on the Find-DbaCommand from the dbatools.io project Original author: Simone Bizzotto (@niphold) #> function Find-FSCPSCommand { [CmdletBinding(SupportsShouldProcess = $true)] param ( [String]$Pattern, [String[]]$Tag, [String]$Author, [String]$MinimumVersion, [String]$MaximumVersion, [switch]$Rebuild, [Alias('Silent')] [switch]$EnableException ) begin { function Get-FSCPSTrimmedString($Text) { return $Text.Trim() -replace '(\r\n){2,}', "`n" } $tagsRex = ([regex]'(?m)^[\s]{0,15}Tags:(.*)$') $authorRex = ([regex]'(?m)^[\s]{0,15}Author:(.*)$') $minverRex = ([regex]'(?m)^[\s]{0,15}MinimumVersion:(.*)$') $maxverRex = ([regex]'(?m)^[\s]{0,15}MaximumVersion:(.*)$') function Get-FSCPSHelp([String]$commandName) { $thishelp = Get-Help $commandName -Full $thebase = @{ } $thebase.CommandName = $commandName $thebase.Name = $thishelp.Name $alias = Get-Alias -Definition $commandName -ErrorAction SilentlyContinue $thebase.Alias = $alias.Name -Join ',' ## fetch the description $thebase.Description = $thishelp.Description.Text ## fetch examples $thebase.Examples = Get-FSCPSTrimmedString -Text ($thishelp.Examples | Out-String -Width 200) ## fetch help link $thebase.Links = ($thishelp.relatedLinks).NavigationLink.Uri ## fetch the synopsis $thebase.Synopsis = $thishelp.Synopsis ## fetch the syntax $thebase.Syntax = Get-FSCPSTrimmedString -Text ($thishelp.Syntax | Out-String -Width 600) ## store notes $as = $thishelp.AlertSet | Out-String -Width 600 ## fetch the tags $tags = $tagsrex.Match($as).Groups[1].Value if ($tags) { $thebase.Tags = $tags.Split(',').Trim() } ## fetch the author $author = $authorRex.Match($as).Groups[1].Value if ($author) { $thebase.Author = $author.Trim() } ## fetch MinimumVersion $MinimumVersion = $minverRex.Match($as).Groups[1].Value if ($MinimumVersion) { $thebase.MinimumVersion = $MinimumVersion.Trim() } ## fetch MaximumVersion $MaximumVersion = $maxverRex.Match($as).Groups[1].Value if ($MaximumVersion) { $thebase.MaximumVersion = $MaximumVersion.Trim() } ## fetch Parameters $parameters = $thishelp.parameters.parameter $command = Get-Command $commandName $params = @() foreach($p in $parameters) { $paramAlias = $command.parameters[$p.Name].Aliases $paramDescr = Get-FSCPSTrimmedString -Text ($p.Description | Out-String -Width 200) $params += , @($p.Name, $paramDescr, ($paramAlias -Join ','), ($p.Required -eq $true), $p.PipelineInput, $p.DefaultValue) } $thebase.Params = $params [pscustomobject]$thebase } function Get-FSCPSIndex() { if ($Pscmdlet.ShouldProcess($dest, "Recreating index")) { $dbamodule = Get-Module -Name fscps.tools $allCommands = $dbamodule.ExportedCommands.Values | Where-Object CommandType -EQ 'Function' $helpcoll = New-Object System.Collections.Generic.List[System.Object] foreach ($command in $allCommands) { $x = Get-FSCPSHelp "$command" $helpcoll.Add($x) } # $dest = Get-DbatoolsConfigValue -Name 'Path.TagCache' -Fallback "$(Resolve-Path $PSScriptRoot\..)\dbatools-index.json" $dest = "$moduleDirectory\bin\fscps.tools-index.json" $helpcoll | ConvertTo-Json -Depth 4 | Out-File $dest -Encoding UTF8 } } $moduleDirectory = (Get-Module -Name fscps.tools).ModuleBase } process { $Pattern = $Pattern.TrimEnd("s") $idxFile = "$moduleDirectory\bin\fscps.tools-index.json" if (!(Test-Path $idxFile) -or $Rebuild) { Write-PSFMessage -Level Verbose -Message "Rebuilding index into $idxFile" $swRebuild = [system.diagnostics.stopwatch]::StartNew() Get-FSCPSIndex Write-PSFMessage -Level Verbose -Message "Rebuild done in $($swRebuild.ElapsedMilliseconds)ms" } $consolidated = Get-Content -Raw $idxFile | ConvertFrom-Json $result = $consolidated if ($Pattern.Length -gt 0) { $result = $result | Where-Object { $_.PsObject.Properties.Value -like "*$Pattern*" } } if ($Tag.Length -gt 0) { foreach ($t in $Tag) { $result = $result | Where-Object Tags -Contains $t } } if ($Author.Length -gt 0) { $result = $result | Where-Object Author -Like "*$Author*" } if ($MinimumVersion.Length -gt 0) { $result = $result | Where-Object MinimumVersion -GE $MinimumVersion } if ($MaximumVersion.Length -gt 0) { $result = $result | Where-Object MaximumVersion -LE $MaximumVersion } Select-DefaultView -InputObject $result -Property CommandName, Synopsis } } <# .SYNOPSIS Get active Azure Storage Account configuration .DESCRIPTION Get active Azure Storage Account configuration object from the configuration store .PARAMETER OutputAsPsCustomObject Instruct the cmdlet to return a PsCustomObject object .EXAMPLE PS C:\> Get-FSCPSActiveAzureStorageConfig This will get the active Azure Storage configuration. .EXAMPLE PS C:\> Get-FSCPSActiveAzureStorageConfig -OutputAsPsCustomObject:$true This will get the active Azure Storage configuration. The object will be output as a PsCustomObject, for you to utilize across your scripts. .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, Container This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCPSActiveAzureStorageConfig { [CmdletBinding()] param ( [switch] $OutputAsPsCustomObject ) $res = Get-PSFConfigValue -FullName "fscps.tools.active.azure.storage.account" if ($OutputAsPsCustomObject) { [PSCustomObject]$res } else { $res } } <# .SYNOPSIS Get Azure Storage Account configs .DESCRIPTION Get all Azure Storage Account configuration objects from the configuration store .PARAMETER Name The name of the Azure Storage Account you are looking for Default value is "*" to display all Azure Storage Account configs .PARAMETER OutputAsHashtable Instruct the cmdlet to return a hastable object .EXAMPLE PS C:\> Get-FSCPSAzureStorageConfig This will show all Azure Storage Account configs .EXAMPLE PS C:\> Get-FSCPSAzureStorageConfig -OutputAsHashtable This will show all Azure Storage Account configs. Every object will be output as a hashtable, for you to utilize as parameters for other cmdlets. .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, Container This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCPSAzureStorageConfig { [CmdletBinding()] param ( [string] $Name = "*", [switch] $OutputAsHashtable ) $StorageAccounts = [hashtable](Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts") if(!$StorageAccounts) { Init-AzureStorageDefault $StorageAccounts = [hashtable](Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts") } foreach ($item in $StorageAccounts.Keys) { if ($item -NotLike $Name) { continue } $res = [ordered]@{Name = $item } $res += $StorageAccounts[$item] if ($OutputAsHashtable) { $res } else { [PSCustomObject]$res } } } <# .SYNOPSIS Get a file from Azure .DESCRIPTION Get all files from an Azure Storage Account .PARAMETER AccountId Storage Account Name / Storage Account Id where you want to look for files .PARAMETER AccessToken The token that has the needed permissions for the search action .PARAMETER SAS The SAS key that you have created for the storage account or blob container .PARAMETER DestinationPath The destination folder of the Azure file to download. If enpty just show the info .PARAMETER Container Name of the blob container inside the storage account you want to look for files .PARAMETER Name Name of the file you are looking for Accepts wildcards for searching. E.g. -Name "Application*Adaptor" Default value is "*" which will search for all packages .PARAMETER Latest Instruct the cmdlet to only fetch the latest file from the Azure Storage Account Latest nugets parameter .EXAMPLE PS C:\> Get-FSCPSAzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" This will get all files in the blob container "backupfiles". It will use the AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" to gain access. .EXAMPLE PS C:\> Get-FSCPSAzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Latest This will get the latest (newest) file from the blob container "backupfiles". It will use the AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" to gain access to the container. .EXAMPLE PS C:\> Get-FSCPSAzureStorageFile -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Name "*UAT*" This will get all files in the blob container "backupfiles" that fits the "*UAT*" search value. It will use the AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" to gain access to the container. .EXAMPLE PS C:\> Get-FSCPSAzureStorageFile -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -Latest This will get the latest (newest) file from the blob container "backupfiles". It will use the SAS key "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" to gain access to the container. .NOTES Tags: Azure, Azure Storage, Token, Blob, File, Container This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCPSAzureStorageFile { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [string] $AccountId = $Script:AzureStorageAccountId, [string] $AccessToken = $Script:AzureStorageAccessToken, [string] $SAS = $Script:AzureStorageSAS, [Alias('Blob')] [Alias('Blobname')] [string] $Container = $Script:AzureStorageContainer, [Parameter(ParameterSetName = 'Default')] [Alias('FileName')] [string] $Name = "*", [Parameter(ParameterSetName = 'Default')] [string] $DestinationPath = "", [Parameter(Mandatory = $true, ParameterSetName = 'Latest')] [Alias('GetLatest')] [switch] $Latest ) if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or ([string]::IsNullOrEmpty($Container)) -or (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) { Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved." Stop-PSFFunction -Message "Stopping because of missing parameters" return } Invoke-TimeSignal -Start if ([string]::IsNullOrEmpty($SAS)) { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken" $storageContext = New-AzStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken } else { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS" $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS) $storageContext = New-AzStorageContext -ConnectionString $conString } try { $files = Get-AzStorageBlob -Container $($Container.ToLower()) -Context $storageContext | Sort-Object -Descending { $_.Properties.LastModified } if ($Latest) { $files | Select-Object -First 1 | Select-PSFObject -TypeName FSCPS.TOOLS.Azure.Blob "name", @{Name = "Size"; Expression = { [PSFSize]$_.Length } }, @{Name = "LastModified"; Expression = { [Datetime]::Parse($_.LastModified) } } } else { foreach ($obj in $files) { if ($obj.Name -NotLike $Name) { continue } if($DestinationPath) { $null = Test-PathExists -Path $DestinationPath -Type Container -Create $destinationBlobPath = (Join-Path $DestinationPath ($obj.Name)) Get-AzStorageBlobContent -Context $storageContext -Container $($Container.ToLower()) -Blob $obj.Name -Destination ($destinationBlobPath) -ConcurrentTaskCount 10 -Force $obj | Select-PSFObject -TypeName FSCPS.TOOLS.Azure.Blob "name", @{Name = "Size"; Expression = { [PSFSize]$_.Length } }, @{Name = "Path"; Expression = { [string]$destinationBlobPath } }, @{Name = "LastModified"; Expression = { [Datetime]::Parse($_.LastModified) } } } else { $obj | Select-PSFObject -TypeName FSCPS.TOOLS.Azure.Blob "name", @{Name = "Size"; Expression = { [PSFSize]$_.Length } }, @{Name = "LastModified"; Expression = { [Datetime]::Parse($_.LastModified) } } } } } } catch { Write-PSFMessage -Level Warning -Message "Something broke" -ErrorRecord $_ } } <# .SYNOPSIS Get the D365FSC NuGet package .DESCRIPTION Get the D365FSC NuGet package from storage account Full list of NuGet: https://lcs.dynamics.com/V2/SharedAssetLibrary and select NuGet packages .PARAMETER Version The version of the NuGet package to download .PARAMETER Type The type of the NuGet package to download .PARAMETER Path The destination folder of the NuGet package to download .PARAMETER Force Instruct the cmdlet to override the package if exists .EXAMPLE PS C:\> Get-FSCPSNuget -Version "10.0.1777.99" -Type PlatformCompilerPackage This will download the NuGet package with version "10.0.1777.99" and type "PlatformCompilerPackage" to the current folder .EXAMPLE PS C:\> Get-FSCPSNuget -Version "10.0.1777.99" -Type PlatformCompilerPackage -Path "c:\temp" This will download the NuGet package with version "10.0.1777.99" and type "PlatformCompilerPackage" to the c:\temp folder .EXAMPLE PS C:\> Get-FSCPSNuget -Version "10.0.1777.99" -Type PlatformCompilerPackage -Path "c:\temp" -Force This will download the NuGet package with version "10.0.1777.99" and type "PlatformCompilerPackage" to the c:\temp folder and override if the package with the same name exists. .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCPSNuget { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignment", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseDeclaredVarsMoreThanAssignments", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string] $Version, [Parameter(Mandatory = $true)] [NuGetType] $Type, [string] $Path, [switch] $Force ) BEGIN { Invoke-TimeSignal -Start $packageName = "" switch ($Type) { ([NugetType]::ApplicationSuiteDevALM) { $packageName = "Microsoft.Dynamics.AX.ApplicationSuite.DevALM.BuildXpp.$Version.nupkg" break; } ([NugetType]::ApplicationDevALM) { $packageName = "Microsoft.Dynamics.AX.Application.DevALM.BuildXpp.$Version.nupkg" break; } ([NugetType]::PlatformDevALM) { $packageName = "Microsoft.Dynamics.AX.Platform.DevALM.BuildXpp.$Version.nupkg" break; } ([NugetType]::PlatformCompilerPackage) { $packageName = "Microsoft.Dynamics.AX.Platform.CompilerPackage.$Version.nupkg" break; } Default {} } $storageConfigs = Get-FSCPSAzureStorageConfig $activeStorageConfigName = "NugetStorage" if($storageConfigs.Length -gt 0) { $activeStorageConfig = Get-FSCPSActiveAzureStorageConfig $storageConfigs | ForEach-Object { if($_.AccountId -eq $activeStorageConfig.AccountId -and $_.Container -eq $activeStorageConfig.Container -and $_.SAS -eq $activeStorageConfig.SAS) { $activeStorageConfigName = $_.Name } } } Write-PSFMessage -Level Verbose -Message "ActiveStorageConfigName: $activeStorageConfigName" if($Force) { $null = Test-PathExists $Path -Create -Type Container } else{ $null = Test-PathExists $Path -Type Container } } PROCESS { if (Test-PSFFunctionInterrupt) { return } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 try { Set-FSCPSActiveAzureStorageConfig "NuGetStorage" -ErrorAction SilentlyContinue $destinationNugetFilePath = Join-Path $Path $packageName $download = (-not(Test-Path $destinationNugetFilePath)) if(!$download) { Write-PSFMessage -Level Host -Message $packageName $blobFile = Get-FSCPSAzureStorageFile -Name $packageName $blobSize = $blobFile.Length $localSize = (Get-Item $destinationNugetFilePath).length Write-PSFMessage -Level Verbose -Message "BlobSize is: $blobSize" Write-PSFMessage -Level Verbose -Message "LocalSize is: $blobSize" $download = $blobSize -ne $localSize } if($Force) { $download = $true } if($download) { Invoke-FSCPSAzureStorageDownload -FileName $packageName -Path $Path -Force:$Force } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while downloading NuGet package" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally{ if((Get-FSCPSAzureStorageConfig $activeStorageConfigName -ErrorAction SilentlyContinue).Length -gt 0){ Set-FSCPSActiveAzureStorageConfig $activeStorageConfigName -ErrorAction SilentlyContinue } else { Set-FSCPSActiveAzureStorageConfig "NuGetStorage" -ErrorAction SilentlyContinue } } } END { Invoke-TimeSignal -End } } <# .SYNOPSIS Get the FSCPS configuration details .DESCRIPTION Get the FSCPS configuration details from the configuration store All settings retrieved from this cmdlets is to be considered the default parameter values across the different cmdlets .PARAMETER SettingsJsonString String contains settings JSON .PARAMETER SettingsJsonPath String contains path to the settings.json .PARAMETER OutputAsHashtable Instruct the cmdlet to return a hashtable object .EXAMPLE PS C:\> Get-FSCPSSettings This will output the current FSCPS configuration. The object returned will be a PSCustomObject. .EXAMPLE PS C:\> Get-FSCPSSettings -OutputAsHashtable This will output the current FSCPS configuration. The object returned will be a Hashtable. .LINK Set-FSCPSSettings .NOTES Tags: Environment, Url, Config, Configuration, LCS, Upload, ClientId Author: Oleksandr Nikolaiev (@onikolaiev) #> function Get-FSCPSSettings { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseOutputTypeCorrectly", "")] param ( [string] $SettingsJsonString, [string] $SettingsJsonPath, [switch] $OutputAsHashtable ) begin{ Invoke-TimeSignal -Start $helperPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\scripts\helpers.ps1" -Resolve . ($helperPath) $res = [Ordered]@{} if((-not ($SettingsJsonString -eq "")) -and (-not ($SettingsJsonPath -eq ""))) { throw "Both settings parameters should not be provided. Please provide only one of them." } if(-not ($SettingsJsonString -eq "")) { $tmpSettingsFilePath = "C:\temp\settings.json" $null = Test-PathExists -Path "C:\temp\" -Type Container -Create $null = Set-Content $tmpSettingsFilePath $SettingsJsonString -Force -PassThru $null = Set-FSCPSSettings -SettingsFilePath $tmpSettingsFilePath } if(-not ($SettingsJsonPath -eq "")) { $null = Set-FSCPSSettings -SettingsFilePath $SettingsJsonPath } } process{ foreach ($config in Get-PSFConfig -FullName "fscps.tools.settings.all.*") { $propertyName = $config.FullName.ToString().Replace("fscps.tools.settings.all.", "") $res.$propertyName = $config.Value } if($Script:IsOnGitHub)# If GitHub context { foreach ($config in Get-PSFConfig -FullName "fscps.tools.settings.github.*") { $propertyName = $config.FullName.ToString().Replace("fscps.tools.settings.github.", "") $res.$propertyName = $config.Value } } if($Script:IsOnAzureDevOps)# If ADO context { foreach ($config in Get-PSFConfig -FullName "fscps.tools.settings.ado.*") { $propertyName = $config.FullName.ToString().Replace("fscps.tools.settings.ado.", "") $res.$propertyName = $config.Value } } if($Script:IsOnLocalhost)# If localhost context { foreach ($config in Get-PSFConfig -FullName "fscps.tools.settings.localhost.*") { $propertyName = $config.FullName.ToString().Replace("fscps.tools.settings.localhost.", "") $res.$propertyName = $config.Value } } if($OutputAsHashtable) { $res } else { [PSCustomObject]$res } } end{ Invoke-TimeSignal -End } } <# .SYNOPSIS Get the list of D365FSC components versions .DESCRIPTION Get the list of D365FSC components versions (NuGets, Packages, Frameworks etc.) .PARAMETER Version The version of the D365FSC .EXAMPLE PS C:\> Get-FSCPSVersionInfo -Version "10.0.39" This will show the list of file versions for the FSCPS module of the 10.0.39 D365FSC. .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> Function Get-FSCPSVersionInfo { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")] [CmdletBinding()] param ( [string] $Version ) BEGIN { Invoke-TimeSignal -Start [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $VersionStrategy = Get-PSFConfigValue -FullName "fscps.tools.settings.all.versionStrategy" $versionsDefaultFile = Join-Path "$Script:DefaultTempPath" "versions.default.json" try { Invoke-FSCPSWebRequest -method GET -Uri "https://raw.githubusercontent.com/fscpscollaborative/fscps/main/Actions/Helpers/versions.default.json" -outFile $versionsDefaultFile } catch { Start-BitsTransfer -Source "https://raw.githubusercontent.com/fscpscollaborative/fscps/main/Actions/Helpers/versions.default.json" -Destination $versionsDefaultFile } $versionsData = (Get-Content $versionsDefaultFile) | ConvertFrom-Json # TODO CREATE GETPROJECTROOTFOLDER function <# $versionsFile = Join-Path $ENV:GITHUB_WORKSPACE '.FSC-PS\versions.json' if(Test-Path $versionsFile) { $versions = (Get-Content $versionsFile) | ConvertFrom-Json ForEach($version in $versions) { ForEach($versionDefault in $versionsData) { if($version.version -eq $versionDefault.version) { if($version.data.PSobject.Properties.name -match "AppVersion") { if($version.data.AppVersion -ne "") { $versionDefault.data.AppVersion = $version.data.AppVersion } } if($version.data.PSobject.Properties.name -match "PlatformVersion") { if($version.data.PlatformVersion -ne "") { $versionDefault.data.PlatformVersion = $version.data.PlatformVersion } } } } } } #> } PROCESS { if (Test-PSFFunctionInterrupt) { return } try { if($Version) { foreach($d in $versionsData) { if($d.version -eq $Version) { $hash = @{ version = $Version data = @{ AppVersion = $( if($VersionStrategy -eq 'GA') { $d.data.AppVersionGA } else { $d.data.AppVersionLatest } ) PlatformVersion = $( if($VersionStrategy -eq 'GA') { $d.data.PlatformVersionGA } else { $d.data.PlatformVersionLatest } ) FSCServiseUpdatePackageId = $d.data.fscServiseUpdatePackageId FSCPreviewVersionPackageId = $d.data.fscPreviewVersionPackageId FSCLatestQualityUpdatePackageId = $d.data.fscLatestQualityUpdatePackageId FSCFinalQualityUpdatePackageId = $d.data.fscFinalQualityUpdatePackageId ECommerceMicrosoftRepoBranch = $d.data.ecommerceMicrosoftRepoBranch } } New-Object PSObject -Property $hash | Select-PSFObject -TypeName "FSCPS.TOOLS.Versions" "*" } } } else { foreach($d in $versionsData) { $hash = @{ version = $d.version data = @{ AppVersion = $( if($VersionStrategy -eq 'GA') { $d.data.AppVersionGA } else { $d.data.AppVersionLatest } ) PlatformVersion = $( if($VersionStrategy -eq 'GA') { $d.data.PlatformVersionGA } else { $d.data.PlatformVersionLatest } ) FSCServiseUpdatePackageId = $d.data.fscServiseUpdatePackageId FSCPreviewVersionPackageId = $d.data.fscPreviewVersionPackageId FSCLatestQualityUpdatePackageId = $d.data.fscLatestQualityUpdatePackageId FSCFinalQualityUpdatePackageId = $d.data.fscFinalQualityUpdatePackageId ECommerceMicrosoftRepoBranch = $d.data.ecommerceMicrosoftRepoBranch } } New-Object PSObject -Property $hash | Select-PSFObject -TypeName "FSCPS.TOOLS.Versions" "*" } } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while getting the versionsData" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally{ } } END { Invoke-TimeSignal -End } } <# .SYNOPSIS Installation of Nuget CLI .DESCRIPTION Download latest Nuget CLI .PARAMETER Path Download destination .PARAMETER Url Url/Uri to where the latest nuget download is located The default value is "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" .EXAMPLE PS C:\> Install-FSCPSNugetCLI -Path "C:\temp\fscps.tools\nuget" -Url "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" This will download the latest version of nuget. .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Install-FSCPSNugetCLI { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [string] $Path = "C:\temp\fscps.tools\nuget", [string] $Url = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" ) begin{ $downloadPath = Join-Path -Path $Path -ChildPath "nuget.exe" if (-not (Test-PathExists -Path $Path -Type Container -Create)) { return } } process{ if (Test-PSFFunctionInterrupt) { return } Write-PSFMessage -Level Verbose -Message "Downloading nuget.exe. $($Url)" -Target $Url (New-Object System.Net.WebClient).DownloadFile($Url, $downloadPath) if (-not (Test-PathExists -Path $downloadPath -Type Leaf)) { return } } end{ Unblock-File -Path $downloadPath Set-PSFConfig -FullName "fscps.tools.path.nuget" -Value $downloadPath Register-PSFConfig -FullName "fscps.tools.path.nuget" Update-ModuleVariables } } <# .SYNOPSIS Function to sign the files with KeyVault .DESCRIPTION Function to sign the files with KeyVault .PARAMETER Uri A fully qualified URL of the key vault with the certificate that will be used for signing. An example value might be https://my-vault.vault.azure.net. .PARAMETER TenantId This is the tenant id used to authenticate to Azure, which will be used to generate an access token. .PARAMETER CertificateName The name of the certificate used to perform the signing operation. .PARAMETER ClientId This is the client ID used to authenticate to Azure, which will be used to generate an access token. .PARAMETER ClientSecret This is the client secret used to authenticate to Azure, which will be used to generate an access token. .PARAMETER TimestampServer A URL to an RFC3161 compliant timestamping service. .PARAMETER FILE A file to sign .EXAMPLE PS C:\> Invoke-FSCPSAzureSignToolSignFile -Uri "https://my-vault.vault.azure.net" ` -TenantId "01234567-abcd-ef012-0000-0123456789ab" ` -CertificateName "my-key-name" ` -ClientId "01234567-abcd-ef012-0000-0123456789ab" ` -ClientSecret "secret" ` -FILE "$filePath" This will sign the target file with the KeyVault certificate .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-FSCPSAzureSignToolSignFile { param ( [Parameter(HelpMessage = "A fully qualified URL of the key vault with the certificate that will be used for signing.", Mandatory = $false)] [string] $Uri, [Parameter(HelpMessage = "This is the tenant id used to authenticate to Azure, which will be used to generate an access token.", Mandatory = $true)] [string] $TenantId, [Parameter(HelpMessage = "The name of the certificate used to perform the signing operation.", Mandatory = $false)] [string] $CertificateName, [Parameter(HelpMessage = "This is the client ID used to authenticate to Azure, which will be used to generate an access token.", Mandatory = $false)] [string] $ClientId, [Parameter(HelpMessage = "This is the client secret used to authenticate to Azure, which will be used to generate an access token.", Mandatory = $true)] [SecureString] $ClientSecret, [Parameter(HelpMessage = "A URL to an RFC3161 compliant timestamping service.", Mandatory = $true)] [string] $TimestampServer = "http://timestamp.digicert.com", [Parameter(HelpMessage = "A file to sign", Mandatory = $true)] [string] $FILE ) begin{ $tempDirectory = "c:\temp" if (!(Test-Path -Path $tempDirectory)) { [System.IO.Directory]::CreateDirectory($tempDirectory) } if(-not (Test-Path $FILE )) { Write-Error "File $FILE is not found! Check the path." exit 1; } try { & dotnet tool install --global AzureSignTool; } catch { Write-PSFMessage -Level Host -Message "Something went wrong while installing AzureSignTool" -Exception $PSItem.Exception } } process{ try { & azuresigntool sign -kvu "$($Uri)" -kvt "$($TenantId)" -kvc "$($CertificateName)" -kvi "$($ClientId)" -kvs "$($ClientSecret)" -tr "$($TimestampServer)" -td sha256 "$FILE" } catch { Write-PSFMessage -Level Host -Message "Something went wrong while signing file. " -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } } end{ } } <# .SYNOPSIS Delete a file to Azure .DESCRIPTION Delete any file to an Azure Storage Account .PARAMETER AccountId Storage Account Name / Storage Account Id where you want to store the file .PARAMETER AccessToken The token that has the needed permissions for the delete action .PARAMETER SAS The SAS key that you have created for the storage account or blob container .PARAMETER Container Name of the blob container inside the storage account you want to store the file .PARAMETER FileName Path to the file you want to delete .PARAMETER Force Instruct the cmdlet to overwrite the file in the container if it already exists .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions This is less user friendly, but allows catching exceptions in calling scripts .EXAMPLE PS C:\> $AzureParams = Get-FSCActiveAzureStorageConfig PS C:\> New-D365Bacpac | Invoke-FSCPSAzureStorageDelete @AzureParams This will get the current Azure Storage Account configuration details and use them as parameters to delete the file from Azure Storage Account. .EXAMPLE PS C:\> Invoke-FSCPSAzureStorageDelete -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -FileName "UAT_20180701.bacpac" This will delete the "UAT_20180701.bacpac" from the "backupfiles" container, inside the "miscfiles" Azure Storage Account. A SAS key is used to gain access to the container and deleteng the file. .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, File, Files, Bacpac, Container Author: Oleksandr Nikolaiev (@onikolaiev) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Invoke-FSCPSAzureStorageDelete { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false)] [string] $AccountId = $Script:AzureStorageAccountId, [Parameter(Mandatory = $false)] [string] $AccessToken = $Script:AzureStorageAccessToken, [Parameter(Mandatory = $false)] [string] $SAS = $Script:AzureStorageSAS, [Parameter(Mandatory = $false)] [Alias('Blob')] [Alias('Blobname')] [string] $Container = $Script:AzureStorageContainer, [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipeline = $true)] [Parameter(Mandatory = $true, ParameterSetName = 'Pipeline', ValueFromPipelineByPropertyName = $true)] [Alias('File')] [string] $FileName, [switch] $Force, [switch] $EnableException ) BEGIN { if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or ([string]::IsNullOrEmpty($Container)) -or (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) { Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved." Stop-PSFFunction -Message "Stopping because of missing parameters" return } } PROCESS { if (Test-PSFFunctionInterrupt) { return } Invoke-TimeSignal -Start try { if ([string]::IsNullOrEmpty($SAS)) { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken" $storageContext = New-AzStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken } else { $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS) Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS" -Target $conString $storageContext = New-AzStorageContext -ConnectionString $conString } Write-PSFMessage -Level Verbose -Message "Start deleting the file from Azure" $files = Get-FSCPSAzureStorageFile -Name $FileName foreach($file in $files) { $null = Remove-AzStorageBlob -Blob $file.Name -Container $($Container.ToLower()) -Context $storageContext -Force:$Force Write-PSFMessage -Level Verbose -Message "The blob $($file.Name) succesfully deleted." } if(-not $files) { Write-PSFMessage -Level Verbose -Message "Files with filter '$($FileName)' were not found in the Storage Account." } } catch { $messageString = "Something went wrong while <c='em'>uploading</c> the file to Azure." Write-PSFMessage -Level Host -Message $messageString -Exception $PSItem.Exception -Target $FileName Stop-PSFFunction -Message "Stopping because of errors." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -ErrorRecord $_ return } finally { Invoke-TimeSignal -End } } END { } } <# .SYNOPSIS Download a file to Azure .DESCRIPTION Download any file to an Azure Storage Account .PARAMETER AccountId Storage Account Name / Storage Account Id where you want to fetch the file from .PARAMETER AccessToken The token that has the needed permissions for the download action .PARAMETER SAS The SAS key that you have created for the storage account or blob container .PARAMETER Container Name of the blob container inside the storage account you where the file is .PARAMETER FileName Name of the file that you want to download .PARAMETER Path Path to the folder / location you want to save the file The default path is "c:\temp\fscps.tools" .PARAMETER Latest Instruct the cmdlet to download the latest file from Azure regardless of name .PARAMETER Force Instruct the cmdlet to overwrite the local file if it already exists .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions This is less user friendly, but allows catching exceptions in calling scripts .EXAMPLE PS C:\> Invoke-FSCPSAzureStorageDownload -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -FileName "OriginalUAT.bacpac" -Path "c:\temp" Will download the "OriginalUAT.bacpac" file from the storage account and save it to "c:\temp\OriginalUAT.bacpac" .EXAMPLE PS C:\> Invoke-FSCPSAzureStorageDownload -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Path "c:\temp" -Latest Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\". The complete path to the file will returned as output from the cmdlet. .EXAMPLE PS C:\> $AzureParams = Get-FSCPSActiveAzureStorageConfig PS C:\> Invoke-FSCPSAzureStorageDownload @AzureParams -Path "c:\temp" -Latest This will get the current Azure Storage Account configuration details and use them as parameters to download the latest file from an Azure Storage Account Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\". The complete path to the file will returned as output from the cmdlet. .EXAMPLE PS C:\> Invoke-FSCPSAzureStorageDownload -Latest This will use the default parameter values that are based on the configuration stored inside "Get-FSCPSActiveAzureStorageConfig". Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\fscps.tools". .EXAMPLE PS C:\> Invoke-FSCPSAzureStorageDownload -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -Path "c:\temp" -Latest Will download the file with the latest modified datetime from the storage account and save it to "c:\temp\". A SAS key is used to gain access to the container and downloading the file from it. The complete path to the file will returned as output from the cmdlet. .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, File, Files, Latest, Bacpac, Container This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Invoke-FSCPSAzureStorageDownload { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false)] [string] $AccountId = $Script:AzureStorageAccountId, [Parameter(Mandatory = $false)] [string] $AccessToken = $Script:AzureStorageAccessToken, [Parameter(Mandatory = $false)] [string] $SAS = $Script:AzureStorageSAS, [Alias('Blob')] [Alias('Blobname')] [string] $Container = $Script:AzureStorageContainer, [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipelineByPropertyName = $true)] [Alias('Name')] [string] $FileName, [string] $Path = $Script:DefaultTempPath, [Parameter(Mandatory = $true, ParameterSetName = 'Latest', Position = 4 )] [Alias('GetLatest')] [switch] $Latest, [switch] $Force, [switch] $EnableException ) BEGIN { if (-not (Test-PathExists -Path $Path -Type Container -Create)) { return } if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or ([string]::IsNullOrEmpty($Container)) -or (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) { Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved." Stop-PSFFunction -Message "Stopping because of missing parameters" return } } PROCESS { if (Test-PSFFunctionInterrupt) { return } Invoke-TimeSignal -Start try { if ([string]::IsNullOrEmpty($SAS)) { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken" $storageContext = New-AzStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken } else { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS" $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS) $storageContext = New-AzStorageContext -ConnectionString $conString } Write-PSFMessage -Level Verbose -Message "Start download from Azure Storage Account" if ($Latest) { $files = Get-AzStorageBlob -Container $($Container.ToLower()) -Context $storageContext $File = ($files | Sort-Object -Descending { $_.LastModified } | Select-Object -First 1) $FileName = $File.Name Write-PSFMessage -Level Verbose -Message "Filename is: $FileName" $NewFile = Join-Path $Path $($File.Name) $null = Get-AzStorageBlobContent -Container $($Container.ToLower()) -Blob $File.Name -Destination $NewFile -Context $storageContext -Force:$Force } else { Write-PSFMessage -Level Verbose -Message "Filename is: $FileName" $NewFile = Join-Path $Path $FileName $null = Get-AzStorageBlobContent -Container $($Container.ToLower()) -Blob $FileName -Destination $NewFile -Context $storageContext -Force:$Force } Get-Item -Path $NewFile | Select-PSFObject "Name as Filename", @{Name = "Size"; Expression = { [PSFSize]$_.Length } }, "LastWriteTime as LastModified", "Fullname as File" } catch { $messageString = "Something went wrong while <c='em'>downloading</c> the file from Azure." Write-PSFMessage -Level Host -Message $messageString -Exception $PSItem.Exception -Target $NewFile Stop-PSFFunction -Message "Stopping because of errors." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -ErrorRecord $_ return } finally { Invoke-TimeSignal -End } } END { } } <# .SYNOPSIS Upload a file to Azure .DESCRIPTION Upload any file to an Azure Storage Account .PARAMETER AccountId Storage Account Name / Storage Account Id where you want to store the file .PARAMETER AccessToken The token that has the needed permissions for the upload action .PARAMETER SAS The SAS key that you have created for the storage account or blob container .PARAMETER Container Name of the blob container inside the storage account you want to store the file .PARAMETER Filepath Path to the file you want to upload .PARAMETER ContentType Media type of the file that is going to be uploaded The value will be used for the blob property "Content Type". If the parameter is left empty, the commandlet will try to automatically determined the value based on the file's extension. If the parameter is left empty and the value cannot be automatically be determined, Azure storage will automatically assign "application/octet-stream" as the content type. Valid media type values can be found here: https://www.iana.org/assignments/media-types/media-types.xhtml .PARAMETER Force Instruct the cmdlet to overwrite the file in the container if it already exists .PARAMETER DeleteOnUpload Switch to tell the cmdlet if you want the local file to be deleted after the upload completes .PARAMETER EnableException This parameters disables user-friendly warnings and enables the throwing of exceptions This is less user friendly, but allows catching exceptions in calling scripts .EXAMPLE PS C:\> Invoke-FSCPSAzureStorageUpload -AccountId "miscfiles" -AccessToken "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" -Container "backupfiles" -Filepath "c:\temp\bacpac\UAT_20180701.bacpac" -DeleteOnUpload This will upload the "c:\temp\bacpac\UAT_20180701.bacpac" up to the "backupfiles" container, inside the "miscfiles" Azure Storage Account that is access with the "xx508xx63817x752xx74004x30705xx92x58349x5x78f5xx34xxxxx51" token. After upload the local file will be deleted. .EXAMPLE PS C:\> $AzureParams = Get-D365ActiveAzureStorageConfig PS C:\> New-D365Bacpac | Invoke-FSCPSAzureStorageUpload @AzureParams This will get the current Azure Storage Account configuration details and use them as parameters to upload the file to an Azure Storage Account. .EXAMPLE PS C:\> New-D365Bacpac | Invoke-FSCPSAzureStorageUpload This will generate a new bacpac file using the "New-D365Bacpac" cmdlet. The file will be uploaded to an Azure Storage Account using the "Invoke-FSCPSAzureStorageUpload" cmdlet. This will use the default parameter values that are based on the configuration stored inside "Get-D365ActiveAzureStorageConfig" for the "Invoke-FSCPSAzureStorageUpload" cmdlet. .EXAMPLE PS C:\> Invoke-FSCPSAzureStorageUpload -AccountId "miscfiles" -SAS "sv2018-03-28&siunlisted&src&sigAUOpdsfpoWE976ASDhfjkasdf(5678sdfhk" -Container "backupfiles" -Filepath "c:\temp\bacpac\UAT_20180701.bacpac" This will upload the "c:\temp\bacpac\UAT_20180701.bacpac" up to the "backupfiles" container, inside the "miscfiles" Azure Storage Account. A SAS key is used to gain access to the container and uploading the file to it. .NOTES Tags: Azure, Azure Storage, Config, Configuration, Token, Blob, File, Files, Bacpac, Container This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) The cmdlet supports piping and can be used in advanced scenarios. See more on github and the wiki pages. #> function Invoke-FSCPSAzureStorageUpload { [CmdletBinding(DefaultParameterSetName = 'Default')] param ( [Parameter(Mandatory = $false)] [string] $AccountId = $Script:AzureStorageAccountId, [Parameter(Mandatory = $false)] [string] $AccessToken = $Script:AzureStorageAccessToken, [Parameter(Mandatory = $false)] [string] $SAS = $Script:AzureStorageSAS, [Parameter(Mandatory = $false)] [Alias('Blob')] [Alias('Blobname')] [string] $Container = $Script:AzureStorageContainer, [Parameter(Mandatory = $true, ParameterSetName = 'Default', ValueFromPipeline = $true)] [Parameter(Mandatory = $true, ParameterSetName = 'Pipeline', ValueFromPipelineByPropertyName = $true)] [Alias('File')] [Alias('Path')] [string] $Filepath, [Parameter(Mandatory = $false)] [string] $ContentType, [switch] $Force, [Parameter(Mandatory = $false)] [switch] $DeleteOnUpload, [switch] $EnableException ) BEGIN { if (([string]::IsNullOrEmpty($AccountId) -eq $true) -or ([string]::IsNullOrEmpty($Container)) -or (([string]::IsNullOrEmpty($AccessToken)) -and ([string]::IsNullOrEmpty($SAS)))) { Write-PSFMessage -Level Host -Message "It seems that you are missing some of the parameters. Please make sure that you either supplied them or have the right configuration saved." Stop-PSFFunction -Message "Stopping because of missing parameters" return } } PROCESS { if (Test-PSFFunctionInterrupt) { return } Invoke-TimeSignal -Start $FileName = Split-Path -Path $Filepath -Leaf try { if ([string]::IsNullOrEmpty($SAS)) { Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with AccessToken" $storageContext = New-AzStorageContext -StorageAccountName $AccountId.ToLower() -StorageAccountKey $AccessToken } else { $conString = $("BlobEndpoint=https://{0}.blob.core.windows.net/;QueueEndpoint=https://{0}.queue.core.windows.net/;FileEndpoint=https://{0}.file.core.windows.net/;TableEndpoint=https://{0}.table.core.windows.net/;SharedAccessSignature={1}" -f $AccountId.ToLower(), $SAS) Write-PSFMessage -Level Verbose -Message "Working against Azure Storage Account with SAS" -Target $conString $storageContext = New-AzStorageContext -ConnectionString $conString } Write-PSFMessage -Level Verbose -Message "Start uploading the file to Azure" if ([string]::IsNullOrEmpty($ContentType)) { $ContentType = Get-MediaTypeByFilename $FileName # Available since .NET4.5, so it can be used with PowerShell 5.0 and higher. Write-PSFMessage -Level Verbose -Message "Content Type is automatically set to value: $ContentType" } $null = Set-AzStorageBlobContent -Context $storageContext -File $Filepath -Container $($Container.ToLower()) -Properties @{"ContentType" = $ContentType} -Force:$Force if ($DeleteOnUpload) { Remove-Item $Filepath -Force } [PSCustomObject]@{ File = $Filepath Filename = $FileName } } catch { $messageString = "Something went wrong while <c='em'>uploading</c> the file to Azure." Write-PSFMessage -Level Host -Message $messageString -Exception $PSItem.Exception -Target $FileName Stop-PSFFunction -Message "Stopping because of errors." -Exception $([System.Exception]::new($($messageString -replace '<[^>]+>', ''))) -ErrorRecord $_ return } finally { Invoke-TimeSignal -End } } END { } } <# .SYNOPSIS Install software from Choco .DESCRIPTION Installs software from Chocolatey Full list of software: https://community.chocolatey.org/packages .PARAMETER Command The command of the choco to execute Support a list of softwares that you want to have installed on the system .PARAMETER Silent Disable output .PARAMETER SkipUpdate Skip the chocolatey update .PARAMETER Command The command of the choco to execute .PARAMETER RemainingArguments List of arguments .PARAMETER Force Force command. Reinstall latest version if command is install or upgrade to latest version .EXAMPLE PS C:\> Invoke-FSCPSChoco install gh -y --allow-unofficial -Silent This will install GH tools on the system without console output .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> Function Invoke-FSCPSChoco { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")] [CmdletBinding()] param ( [Parameter(Mandatory = $true, Position = 0)] [string] $Command, [Parameter(Mandatory = $false, Position = 1, ValueFromRemainingArguments = $true)] $RemainingArguments, [switch] $Silent, [switch] $SkipUpdate, [switch] $Force ) BEGIN { Invoke-TimeSignal -Start try { if (Test-Path -Path "$env:ProgramData\Chocolatey") { if($SkipUpdate) { if (!$Silent) { choco upgrade chocolatey -y -r choco upgrade all --ignore-checksums -y -r } else{ $null = choco upgrade chocolatey -y -r -silent $null = choco upgrade all --ignore-checksums -y -r } } } else { Write-PSFMessage -Level InternalComment -Message "Installing Chocolatey" # Download and execute installation script [System.Net.WebRequest]::DefaultWebProxy.Credentials = [System.Net.CredentialCache]::DefaultCredentials Invoke-Expression ((New-Object System.Net.WebClient).DownloadString("https://chocolatey.org/install.ps1")) } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while installing or updating Chocolatey" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } #Determine choco executable location # This is needed because the path variable is not updated in this session yet # This part is copied from https://chocolatey.org/install.ps1 $chocoPath = [Environment]::GetEnvironmentVariable("ChocolateyInstall") if ($chocoPath -eq $null -or $chocoPath -eq '') { $chocoPath = "$env:ALLUSERSPROFILE\Chocolatey" } if (!(Test-Path ($chocoPath))) { $chocoPath = "$env:SYSTEMDRIVE\ProgramData\Chocolatey" } $chocoExePath = Join-Path $chocoPath 'bin\choco.exe' if (-not (Test-PathExists -Path $chocoExePath -Type Leaf)) { return } } PROCESS { if (Test-PSFFunctionInterrupt) { return } try { foreach ($item in $Name) { Write-PSFMessage -Level InternalComment -Message "Installing $item" $arguments = New-Object System.Collections.Generic.List[System.Object] $arguments.Add("$Command ") $RemainingArguments | ForEach-Object { if ("$_".IndexOf(" ") -ge 0 -or "$_".IndexOf('"') -ge 0) { $arguments.Add("""$($_.Replace('"','\"'))"" ") } else { $arguments.Add("$_ ") } } if ($Force) { $arguments.Add("-f") } if (!$Silent) { Invoke-Process -Executable $chocoExePath -Params $($arguments.ToArray()) -ShowOriginalProgress:$true } else { $null = Invoke-Process -Executable $chocoExePath -Params $($arguments.ToArray()) -ShowOriginalProgress:$false } } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while installing software" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return } finally{ } } END { Invoke-TimeSignal -End } } <# .SYNOPSIS Invoke the D365FSC models compilation .DESCRIPTION Invoke the D365FSC models compilation .PARAMETER Version The version of the D365FSC used to build .PARAMETER Type The type of the FSCPS project to build .PARAMETER SourcesPath The folder contains a metadata files with binaries .PARAMETER BuildFolderPath The destination build folder .PARAMETER OutputAsHashtable Instruct the cmdlet to return a hashtable object .PARAMETER Force Cleanup destination build folder befor build .EXAMPLE PS C:\> Invoke-FSCPSCompile -Version "10.0.39" -Type FSCM Example output: METADATA_DIRECTORY : D:\a\8\s\Metadata FRAMEWORK_DIRECTORY : C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.CompilerPackage.7.0.7120.99 BUILD_OUTPUT_DIRECTORY : C:\temp\buildbuild\bin NUGETS_FOLDER : C:\temp\buildbuild\packages BUILD_LOG_FILE_PATH : C:\Users\VssAdministrator\AppData\Local\Temp\Build.sln.msbuild.log PACKAGE_NAME : MAIN TEST-DeployablePackage-10.0.39-78 PACKAGE_PATH : C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip ARTIFACTS_PATH : C:\temp\buildbuild\artifacts ARTIFACTS_LIST : ["C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip"] This will build D365FSC package with version "10.0.39" to the Temp folder .EXAMPLE PS C:\> Invoke-FSCPSCompile -Version "10.0.39" -Path "c:\Temp" Example output: METADATA_DIRECTORY : D:\a\8\s\Metadata FRAMEWORK_DIRECTORY : C:\temp\buildbuild\packages\Microsoft.Dynamics.AX.Platform.CompilerPackage.7.0.7120.99 BUILD_OUTPUT_DIRECTORY : C:\temp\buildbuild\bin NUGETS_FOLDER : C:\temp\buildbuild\packages BUILD_LOG_FILE_PATH : C:\Users\VssAdministrator\AppData\Local\Temp\Build.sln.msbuild.log PACKAGE_NAME : MAIN TEST-DeployablePackage-10.0.39-78 PACKAGE_PATH : C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip ARTIFACTS_PATH : C:\temp\buildbuild\artifacts ARTIFACTS_LIST : ["C:\temp\buildbuild\artifacts\MAIN TEST-DeployablePackage-10.0.39-78.zip"] This will build D365FSC package with version "10.0.39" to the Temp folder .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-FSCPSCompile { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingInvokeExpression", "")] [CmdletBinding()] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [string] $Version, [Parameter(Mandatory = $true)] [string] $SourcesPath, [FSCPSType]$Type, [string] $BuildFolderPath = (Join-Path $script:DefaultTempPath _bld), [switch] $OutputAsHashtable, [switch] $Force ) BEGIN { Invoke-TimeSignal -Start try { $settings = Get-FSCPSSettings -OutputAsHashtable $responseObject = [Ordered]@{} if($settings.type -eq '' -and ($null -eq $Type)) { throw "Project type should be provided!" } if($settings.type -eq '') { $settings.type = $Type } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while compiling " -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } } PROCESS { if (Test-PSFFunctionInterrupt) { return } try { switch($settings.type) { 'FSCM' { $responseObject = (Invoke-FSCCompile -Version $Version -SourcesPath $SourcesPath -BuildFolderPath $BuildFolderPath -Force:$Force ) break; } 'ECommerce' { #$responseObject = (Invoke-ECommerceCompile -Version $Version -SourcesPath $SourcesPath -BuildFolderPath $BuildFolderPath -Force:$Force) #break; } 'Commerce' { $responseObject = (Invoke-CommerceCompile -Version $Version -SourcesPath $SourcesPath -BuildFolderPath $BuildFolderPath -Force:$Force) break; } Default{ throw "Project type should be provided!" } } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while compiling " -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } finally{ if($OutputAsHashtable) { $responseObject } else { [PSCustomObject]$responseObject } } } END { Invoke-TimeSignal -End } } <# .SYNOPSIS Function to sign the files with digicert .DESCRIPTION Function to sign the files with digicert .PARAMETER SM_HOST Digicert host URL. Default value "https://clientauth.one.digicert.com" .PARAMETER SM_API_KEY The DigiCert API Key .PARAMETER SM_CLIENT_CERT_FILE The DigiCert certificate local path (p12) .PARAMETER SM_CLIENT_CERT_FILE_URL The DigiCert certificate URL (p12) .PARAMETER SM_CLIENT_CERT_PASSWORD The DigiCert certificate password .PARAMETER SM_CODE_SIGNING_CERT_SHA1_HASH The DigiCert certificate thumbprint(fingerprint) .PARAMETER FILE A file to sign .EXAMPLE PS C:\> Invoke-FSCPSDigiCertSignFile -SM_API_KEY "$codeSignDigiCertAPISecretName" ` -SM_CLIENT_CERT_FILE_URL "$codeSignDigiCertUrlSecretName" ` -SM_CLIENT_CERT_PASSWORD $(ConvertTo-SecureString $codeSignDigiCertPasswordSecretName -AsPlainText -Force) ` -SM_CODE_SIGNING_CERT_SHA1_HASH "$codeSignDigiCertHashSecretName" ` -FILE "$filePath" This will sign the target file with the DigiCert certificate .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-FSCPSDigiCertSignFile { param ( [Parameter(HelpMessage = "The DigiCert host", Mandatory = $false)] [string] $SM_HOST = "https://clientauth.one.digicert.com", [Parameter(HelpMessage = "The DigiCert API Key", Mandatory = $true)] [string] $SM_API_KEY, [Parameter(HelpMessage = "The DigiCert certificate local path (p12)", Mandatory = $false)] [string] $SM_CLIENT_CERT_FILE = "c:\temp\digicert.p12", [Parameter(HelpMessage = "The DigiCert certificate URL (p12)", Mandatory = $false)] [string] $SM_CLIENT_CERT_FILE_URL, [Parameter(HelpMessage = "The DigiCert certificate password", Mandatory = $true)] [SecureString] $SM_CLIENT_CERT_PASSWORD, [Parameter(HelpMessage = "The DigiCert certificate thumbprint(fingerprint)", Mandatory = $true)] [string] $SM_CODE_SIGNING_CERT_SHA1_HASH, [Parameter(HelpMessage = "A file to sign", Mandatory = $true)] [string] $FILE ) begin{ $tempDirectory = "c:\temp" if (!(Test-Path -Path $tempDirectory)) { [System.IO.Directory]::CreateDirectory($tempDirectory) } $certLocation = "$tempDirectory\digicert.p12" if(-not (Test-Path $FILE )) { Write-Error "File $FILE is not found! Check the path." exit 1; } if(-not (Test-Path $SM_CLIENT_CERT_FILE )) { if(![string]::IsNullOrEmpty($SM_CLIENT_CERT_FILE_URL)) { $certLocation = Join-Path $tempDirectory "digiCert.p12" Invoke-WebRequest -Uri "$SM_CLIENT_CERT_FILE_URL" -OutFile $certLocation if(Test-Path $certLocation) { $SM_CLIENT_CERT_FILE = $certLocation } } if(-not (Test-Path $SM_CLIENT_CERT_FILE )) { Write-Error "Certificate $SM_CLIENT_CERT_FILE is not found! Check the path." exit 1; } } $currentLocation = Get-Location $signMessage = "" #set env variables $env:SM_CLIENT_CERT_FILE = $SM_CLIENT_CERT_FILE $env:SM_HOST = $SM_HOST $env:SM_API_KEY = $SM_API_KEY $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SM_CLIENT_CERT_PASSWORD) $UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) $env:SM_CLIENT_CERT_PASSWORD = $UnsecurePassword Set-Location $tempDirectory if(-not (Test-Path -Path .\smtools-windows-x64.msi )) { Write-Output "===============smtools-windows-x64.msi================" $smtools = "smtools-windows-x64.msi" Write-Output "The '$smtools' not found. Downloading..." Invoke-WebRequest -Method Get https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -Headers @{ "x-api-key" = "$($SM_API_KEY)"} -OutFile .\$smtools -Verbose Write-Output "Downloaded. Installing..." msiexec /i $smtools /quiet /qn /le smtools.log Get-Content smtools.log -ErrorAction SilentlyContinue Write-Output "Installed." Start-Sleep -Seconds 5 } Write-Output "Checking DigiCert location..." $smctlLocation = (Get-ChildItem "$Env:Programfiles\DigiCert" -Recurse | Where-Object { $_.BaseName -like "smctl" }) if(Test-Path $smctlLocation.FullName) { Write-Output "DigiCert directory found at: $($smctlLocation.Directory)" } else { Write-Error "DigiCert directory not found. Check the installation." exit 1 } $appCertKitPath = "${env:ProgramFiles(x86)}\Windows Kits\10\App Certification Kit" Set-PathVariable -Scope Process -RemovePath $appCertKitPath -ErrorAction SilentlyContinue Set-PathVariable -Scope Process -AddPath $appCertKitPath -ErrorAction SilentlyContinue & certutil.exe -csp "DigiCert Software Trust Manager KSP" -key -user & $($smctlLocation.FullName) windows certsync } process{ try { try { if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent){ Write-Output "===============Healthcheck================" & $($smctlLocation.FullName) healthcheck Write-Output "===============KeyPair list================" & $($smctlLocation.FullName) keypair ls } } catch { Write-Output "Healchcheck failed. please check it" } Write-Output "Set-Location of DigiCert" Set-Location $($smctlLocation.Directory) $signMessage = $(& $($smctlLocation.FullName) sign --fingerprint $SM_CODE_SIGNING_CERT_SHA1_HASH --input $FILE --verbose) Write-Output $($signMessage) if($signMessage.Contains("FAILED")){ Write-Output (Get-Content "$env:USERPROFILE\.signingmanager\logs\smctl.log" -ErrorAction SilentlyContinue) throw; } if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent){ & $($smctlLocation.FullName) sign verify --input $FILE } Write-Output "File '$($FILE)' was signed successful!" } catch { Write-Output "Something went wrong! Read the healthcheck." # & $smctlLocation.FullName healthcheck } } end{ Clear-Content $env:SM_HOST -Force -ErrorAction SilentlyContinue Clear-Content $env:SM_API_KEY -Force -ErrorAction SilentlyContinue Clear-Content $env:SM_CLIENT_CERT_PASSWORD -Force -ErrorAction SilentlyContinue Set-Location $currentLocation if((Test-Path $certLocation )) { Remove-Item $certLocation -Force -ErrorAction SilentlyContinue } } } <# .SYNOPSIS Function to sign the files with digicert .DESCRIPTION Function to sign the files with digicert .PARAMETER SM_HOST Digicert host URL. Default value "https://clientauth.one.digicert.com" .PARAMETER SM_API_KEY The DigiCert API Key .PARAMETER SM_CLIENT_CERT_FILE The DigiCert certificate local path (p12) .PARAMETER SM_CLIENT_CERT_FILE_URL The DigiCert certificate URL (p12) .PARAMETER SM_CLIENT_CERT_PASSWORD The DigiCert certificate password .PARAMETER SM_CODE_SIGNING_CERT_SHA1_HASH The DigiCert certificate thumbprint(fingerprint) .PARAMETER FILE A file to sign .EXAMPLE PS C:\> Invoke-FSCPSSignBinaryFile -SM_API_KEY "$codeSignDigiCertAPISecretName" ` -SM_CLIENT_CERT_FILE_URL "$codeSignDigiCertUrlSecretName" ` -SM_CLIENT_CERT_PASSWORD $(ConvertTo-SecureString $codeSignDigiCertPasswordSecretName -AsPlainText -Force) ` -SM_CODE_SIGNING_CERT_SHA1_HASH "$codeSignDigiCertHashSecretName" ` -FILE "$filePath" This will sign the target file with the DigiCert certificate .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Invoke-FSCPSSignBinaryFile { param ( [Parameter(HelpMessage = "The DigiCert host", Mandatory = $false)] [string] $SM_HOST = "https://clientauth.one.digicert.com", [Parameter(HelpMessage = "The DigiCert API Key", Mandatory = $true)] [string] $SM_API_KEY, [Parameter(HelpMessage = "The DigiCert certificate local path (p12)", Mandatory = $false)] [string] $SM_CLIENT_CERT_FILE = "c:\temp\digicert.p12", [Parameter(HelpMessage = "The DigiCert certificate URL (p12)", Mandatory = $false)] [string] $SM_CLIENT_CERT_FILE_URL, [Parameter(HelpMessage = "The DigiCert certificate password", Mandatory = $true)] [SecureString] $SM_CLIENT_CERT_PASSWORD, [Parameter(HelpMessage = "The DigiCert certificate thumbprint(fingerprint)", Mandatory = $true)] [string] $SM_CODE_SIGNING_CERT_SHA1_HASH, [Parameter(HelpMessage = "A file to sign", Mandatory = $true)] [string] $FILE ) begin{ $tempDirectory = "c:\temp" if (!(Test-Path -Path $tempDirectory)) { [System.IO.Directory]::CreateDirectory($tempDirectory) } $certLocation = "$tempDirectory\digicert.p12" if(-not (Test-Path $FILE )) { Write-Error "File $FILE is not found! Check the path." exit 1; } if(-not (Test-Path $SM_CLIENT_CERT_FILE )) { if(![string]::IsNullOrEmpty($SM_CLIENT_CERT_FILE_URL)) { $certLocation = Join-Path $tempDirectory "digiCert.p12" Invoke-WebRequest -Uri "$SM_CLIENT_CERT_FILE_URL" -OutFile $certLocation if(Test-Path $certLocation) { $SM_CLIENT_CERT_FILE = $certLocation } } if(-not (Test-Path $SM_CLIENT_CERT_FILE )) { Write-Error "Certificate $SM_CLIENT_CERT_FILE is not found! Check the path." exit 1; } } $currentLocation = Get-Location $signMessage = "" #set env variables $env:SM_CLIENT_CERT_FILE = $SM_CLIENT_CERT_FILE $env:SM_HOST = $SM_HOST $env:SM_API_KEY = $SM_API_KEY $BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($SM_CLIENT_CERT_PASSWORD) $UnsecurePassword = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR) $env:SM_CLIENT_CERT_PASSWORD = $UnsecurePassword Set-Location $tempDirectory if(-not (Test-Path -Path .\smtools-windows-x64.msi )) { Write-Output "===============smtools-windows-x64.msi================" $smtools = "smtools-windows-x64.msi" Write-Output "The '$smtools' not found. Downloading..." Invoke-WebRequest -Method Get https://one.digicert.com/signingmanager/api-ui/v1/releases/smtools-windows-x64.msi/download -Headers @{ "x-api-key" = "$($SM_API_KEY)"} -OutFile .\$smtools -Verbose Write-Output "Downloaded. Installing..." msiexec /i $smtools /quiet /qn /le smtools.log Get-Content smtools.log -ErrorAction SilentlyContinue Write-Output "Installed." Start-Sleep -Seconds 5 } Write-Output "Checking DigiCert location..." $smctlLocation = (Get-ChildItem "$Env:Programfiles\DigiCert" -Recurse | Where-Object { $_.BaseName -like "smctl" }) if(Test-Path $smctlLocation.FullName) { Write-Output "DigiCert directory found at: $($smctlLocation.Directory)" } else { Write-Error "DigiCert directory not found. Check the installation." exit 1 } $appCertKitPath = "${env:ProgramFiles(x86)}\Windows Kits\10\App Certification Kit" Set-PathVariable -Scope Process -RemovePath $appCertKitPath -ErrorAction SilentlyContinue Set-PathVariable -Scope Process -AddPath $appCertKitPath -ErrorAction SilentlyContinue & certutil.exe -csp "DigiCert Software Trust Manager KSP" -key -user & $($smctlLocation.FullName) windows certsync } process{ try { try { if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent){ Write-Output "===============Healthcheck================" & $($smctlLocation.FullName) healthcheck Write-Output "===============KeyPair list================" & $($smctlLocation.FullName) keypair ls } } catch { Write-Output "Healchcheck failed. please check it" } Write-Output "Set-Location of DigiCert" Set-Location $($smctlLocation.Directory) $signMessage = $(& $($smctlLocation.FullName) sign --fingerprint $SM_CODE_SIGNING_CERT_SHA1_HASH --input $FILE --verbose) Write-Output $($signMessage) if($signMessage.Contains("FAILED")){ Write-Output (Get-Content "$env:USERPROFILE\.signingmanager\logs\smctl.log" -ErrorAction SilentlyContinue) throw; } if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent){ & $($smctlLocation.FullName) sign verify --input $FILE } Write-Output "File '$($FILE)' was signed successful!" } catch { Write-Output "Something went wrong! Read the healthcheck." # & $smctlLocation.FullName healthcheck } } end{ Clear-Content $env:SM_HOST -Force -ErrorAction SilentlyContinue Clear-Content $env:SM_API_KEY -Force -ErrorAction SilentlyContinue Clear-Content $env:SM_CLIENT_CERT_PASSWORD -Force -ErrorAction SilentlyContinue Set-Location $currentLocation if((Test-Path $certLocation )) { Remove-Item $certLocation -Force -ErrorAction SilentlyContinue } } } <# .SYNOPSIS Register Azure Storage Configurations .DESCRIPTION Register all Azure Storage Configurations .PARAMETER ConfigStorageLocation Parameter used to instruct where to store the configuration objects The default value is "User" and this will store all configuration for the active user Valid options are: "User" "System" "System" will store the configuration as default for all users, so they can access the configuration objects .EXAMPLE PS C:\> Register-FSCPSAzureStorageConfig -ConfigStorageLocation "System" This will store all Azure Storage Configurations as defaults for all users on the machine. .NOTES Tags: Configuration, Azure, Storage This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) #> function Register-FSCPSAzureStorageConfig { [CmdletBinding()] [OutputType()] param ( [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User" ) $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation Register-PSFConfig -FullName "fscps.tools.azure.storage.accounts" -Scope $configScope } <# .SYNOPSIS Set the active Azure Storage Account configuration .DESCRIPTION Updates the current active Azure Storage Account configuration with a new one .PARAMETER Name The name the Azure Storage Account configuration you want to load into the active Azure Storage Account configuration .PARAMETER ConfigStorageLocation Parameter used to instruct where to store the configuration objects The default value is "User" and this will store all configuration for the active user Valid options are: "User" "System" "System" will store the configuration so all users can access the configuration objects .PARAMETER Temporary Instruct the cmdlet to only temporarily override the persisted settings in the configuration storage .EXAMPLE PS C:\> Set-FSCPSActiveAzureStorageConfig -Name "UAT-Exports" This will import the "UAT-Exports" set from the Azure Storage Account configurations. It will update the active Azure Storage Account configuration. .EXAMPLE PS C:\> Set-FSCPSActiveAzureStorageConfig -Name "UAT-Exports" -ConfigStorageLocation "System" This will import the "UAT-Exports" set from the Azure Storage Account configurations. It will update the active Azure Storage Account configuration. The data will be stored in the system wide configuration storage, which makes it accessible from all users. .EXAMPLE PS C:\> Set-FSCPSActiveAzureStorageConfig -Name "UAT-Exports" -Temporary This will import the "UAT-Exports" set from the Azure Storage Account configurations. It will update the active Azure Storage Account configuration. The update will only last for the rest of this PowerShell console session. .NOTES This is refactored function from d365fo.tools Original Author: Mötz Jensen (@Splaxi) Author: Oleksandr Nikolaiev (@onikolaiev) You will have to run the Add-FSCPSAzureStorageConfig cmdlet at least once, before this will be capable of working. #> function Set-FSCPSActiveAzureStorageConfig { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [string] $Name, [ValidateSet('User', 'System')] [string] $ConfigStorageLocation = "User", [switch] $Temporary ) $configScope = Test-ConfigStorageLocation -ConfigStorageLocation $ConfigStorageLocation if (Test-PSFFunctionInterrupt) { return } $azureStorageConfigs = [hashtable] (Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts") if(!$azureStorageConfigs) { Init-AzureStorageDefault $azureStorageConfigs = [hashtable](Get-PSFConfigValue -FullName "fscps.tools.azure.storage.accounts") } if (-not ($azureStorageConfigs.ContainsKey($Name))) { Write-PSFMessage -Level Host -Message "An Azure Storage Account with that name <c='$Name'> doesn't exists</c>." Stop-PSFFunction -Message "Stopping because an Azure Storage Account with that name doesn't exists." return } else { $azureDetails = $azureStorageConfigs[$Name] Set-PSFConfig -FullName "fscps.tools.active.azure.storage.account" -Value $azureDetails if (-not $Temporary) { Register-PSFConfig -FullName "fscps.tools.active.azure.storage.account" -Scope $configScope } Update-AzureStorageVariables } } <# .SYNOPSIS Set the FSCPS configuration details .DESCRIPTION Set the FSCPS configuration details from the configuration store All settings retrieved from this cmdlets is to be considered the default parameter values across the different cmdlets .PARAMETER SettingsJsonString String contains JSON with custom settings .PARAMETER SettingsFilePath Set path to the settings.json file .EXAMPLE PS C:\> Set-FSCPSSettings -SettingsFilePath "c:\temp\settings.json" This will output the current FSCPS configuration. The object returned will be a Hashtable. .LINK Get-FSCPSSettings .NOTES Tags: Environment, Url, Config, Configuration, Upload, ClientId, Settings Author: Oleksandr Nikolaiev (@onikolaiev) #> function Set-FSCPSSettings { [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseSingularNouns", "")] [OutputType([System.Collections.Specialized.OrderedDictionary])] param ( [string] $SettingsFilePath, [string] $SettingsJsonString ) begin{ if((-not ($SettingsJsonString -eq "")) -and (-not ($SettingsFilePath -eq ""))) { throw "Both settings parameters cannot be provided. Please provide only one of them." } if(-not ($SettingsJsonString -eq "")) { $SettingsFilePath = "C:\temp\settings.json" $null = Test-PathExists -Path "C:\temp\" -Type Container -Create $null = Set-Content $SettingsFilePath $SettingsJsonString -Force -PassThru } $fscpsFolderName = Get-PSFConfigValue -FullName "fscps.tools.settings.all.fscpsFolder" $fscmSettingsFile = Get-PSFConfigValue -FullName "fscps.tools.settings.all.fscpsSettingsFile" $fscmRepoSettingsFile = Get-PSFConfigValue -FullName "fscps.tools.settings.all.fscpsRepoSettingsFile" Write-PSFMessage -Level Verbose -Message "fscpsFolderName is: $fscpsFolderName" Write-PSFMessage -Level Verbose -Message "fscmSettingsFile is: $fscmSettingsFile" Write-PSFMessage -Level Verbose -Message "fscmRepoSettingsFile is: $fscmRepoSettingsFile" $settingsFiles = @() $res = [Ordered]@{} $reposytoryName = "" $reposytoryOwner = "" $currentBranchName = "" if($Script:IsOnGitHub)# If GitHub context { Write-PSFMessage -Level Important -Message "Running on GitHub" Set-PSFConfig -FullName 'fscps.tools.settings.all.repoProvider' -Value 'GitHub' Set-PSFConfig -FullName 'fscps.tools.settings.all.repositoryRootPath' -Value "$env:GITHUB_WORKSPACE" Set-PSFConfig -FullName 'fscps.tools.settings.all.runId' -Value "$ENV:GITHUB_RUN_NUMBER" Set-PSFConfig -FullName 'fscps.tools.settings.all.workflowName' -Value "$ENV:GITHUB_WORKFLOW" if($SettingsFilePath -eq "") { $RepositoryRootPath = "$env:GITHUB_WORKSPACE" Write-PSFMessage -Level Verbose -Message "GITHUB_WORKSPACE is: $RepositoryRootPath" $settingsFiles += (Join-Path $fscpsFolderName $fscmSettingsFile) } else{ $settingsFiles += $SettingsFilePath } $reposytoryOwner = "$env:GITHUB_REPOSITORY".Split("/")[0] $reposytoryName = "$env:GITHUB_REPOSITORY".Split("/")[1] Write-PSFMessage -Level Verbose -Message "GITHUB_REPOSITORY is: $reposytoryName" $branchName = "$env:GITHUB_REF" Write-PSFMessage -Level Verbose -Message "GITHUB_REF is: $branchName" $currentBranchName = [regex]::Replace($branchName.Replace("refs/heads/","").Replace("/","_"), '(?i)(?:^|-|_)(\p{L})', { $args[0].Groups[1].Value.ToUpper()}) $gitHubFolder = ".github" $workflowName = "$env:GITHUB_WORKFLOW" Write-PSFMessage -Level Verbose -Message "GITHUB_WORKFLOW is: $workflowName" $workflowName = ($workflowName.Split([System.IO.Path]::getInvalidFileNameChars()) -join "").Replace("(", "").Replace(")", "").Replace("/", "") $settingsFiles += (Join-Path $gitHubFolder $fscmRepoSettingsFile) $settingsFiles += (Join-Path $gitHubFolder "$workflowName.settings.json") } elseif($Script:IsOnAzureDevOps)# If Azure DevOps context { Write-PSFMessage -Level Verbose -Message "Running on Azure" Set-PSFConfig -FullName 'fscps.tools.settings.all.repoProvider' -Value 'AzureDevOps' Set-PSFConfig -FullName 'fscps.tools.settings.all.repositoryRootPath' -Value "$env:PIPELINE_WORKSPACE" Set-PSFConfig -FullName 'fscps.tools.settings.all.runId' -Value "$ENV:Build_BuildNumber" Set-PSFConfig -FullName 'fscps.tools.settings.all.workflowName' -Value "$ENV:Build_DefinitionName" if($SettingsFilePath -eq "") { $RepositoryRootPath = "$env:PIPELINE_WORKSPACE" Write-PSFMessage -Level Verbose -Message "RepositoryRootPath is: $RepositoryRootPath" } else{ $settingsFiles += $SettingsFilePath } $reposytoryOwner = $($env:SYSTEM_TEAMFOUNDATIONCOLLECTIONURI.replace('https://dev.azure.com/', '').replace('/', '').replace('https:','')) $reposytoryName = "$env:SYSTEM_TEAMPROJECT" $branchName = "$env:BUILD_SOURCEBRANCH" $currentBranchName = [regex]::Replace($branchName.Replace("/Metadata","").Replace("$/$($reposytoryName)/","").Replace("$/$($reposytoryName)","").Replace("Trunk/","").Replace("/","_"), '(?i)(?:^|-|_)(\p{L})', { $args[0].Groups[1].Value.ToUpper() }) #$settingsFiles += (Join-Path $fscpsFolderName $fscmSettingsFile) } else { # If Desktop or other Write-PSFMessage -Level Verbose -Message "Running on desktop" Set-PSFConfig -FullName 'fscps.tools.settings.all.repoProvider' -Value 'Other' if($SettingsFilePath -eq "") { throw "SettingsFilePath variable should be passed if running on the cloud/personal computer" } $reposytoryName = "windows host" Set-PSFConfig -FullName 'fscps.tools.settings.all.runId' -Value 1 $currentBranchName = 'DEV' $settingsFiles += $SettingsFilePath } Set-PSFConfig -FullName 'fscps.tools.settings.all.currentBranch' -Value $currentBranchName Set-PSFConfig -FullName 'fscps.tools.settings.all.repoOwner' -Value $reposytoryOwner Set-PSFConfig -FullName 'fscps.tools.settings.all.repoName' -Value $reposytoryName function MergeCustomObjectIntoOrderedDictionary { Param( [System.Collections.Specialized.OrderedDictionary] $dst, [PSCustomObject] $src ) # Add missing properties in OrderedDictionary $src.PSObject.Properties.GetEnumerator() | ForEach-Object { $prop = $_.Name $srcProp = $src."$prop" $srcPropType = $srcProp.GetType().Name if (-not $dst.Contains($prop)) { if ($srcPropType -eq "PSCustomObject") { $dst.Add("$prop", [ordered]@{}) } elseif ($srcPropType -eq "Object[]") { $dst.Add("$prop", @()) } else { $dst.Add("$prop", $srcProp) } } } @($dst.Keys) | ForEach-Object { $prop = $_ if ($src.PSObject.Properties.Name -eq $prop) { $dstProp = $dst."$prop" $srcProp = $src."$prop" $dstPropType = $dstProp.GetType().Name $srcPropType = $srcProp.GetType().Name if($dstPropType -eq 'Int32' -and $srcPropType -eq 'Int64') { $dstPropType = 'Int64' } if ($srcPropType -eq "PSCustomObject" -and $dstPropType -eq "OrderedDictionary") { MergeCustomObjectIntoOrderedDictionary -dst $dst."$prop".Value -src $srcProp } elseif ($dstPropType -ne $srcPropType) { throw "property $prop should be of type $dstPropType, is $srcPropType." } else { if ($srcProp -is [Object[]]) { $srcProp | ForEach-Object { $srcElm = $_ $srcElmType = $srcElm.GetType().Name if ($srcElmType -eq "PSCustomObject") { $ht = [ordered]@{} $srcElm.PSObject.Properties | Sort-Object -Property Name -Culture "iv-iv" | ForEach-Object { $ht[$_.Name] = $_.Value } $dst."$prop" += @($ht) } else { $dst."$prop" += $srcElm } } } else { Write-PSFMessage -Level Verbose -Message "Searching fscps.tools.settings.*.$prop" $setting = Get-PSFConfig -FullName "fscps.tools.settings.*.$prop" Write-PSFMessage -Level Verbose -Message "Found $setting" if($setting) { Set-PSFConfig -FullName $setting.FullName -Value $srcProp } #$dst."$prop" = $srcProp } } } } } } process{ Invoke-TimeSignal -Start $res = Get-FSCPSSettings -OutputAsHashtable $settingsFiles | ForEach-Object { $settingsFile = $_ if($RepositoryRootPath) { $settingsPath = Join-Path $RepositoryRootPath $settingsFile } else { $settingsPath = $SettingsFilePath } Write-PSFMessage -Level Verbose -Message "Settings file '$settingsFile' - $(If (Test-Path $settingsPath) {"exists. Processing..."} Else {"not exists. Skip."})" if (Test-Path $settingsPath) { try { $settingsJson = Get-Content $settingsPath -Encoding UTF8 | ConvertFrom-Json # check settingsJson.version and do modifications if needed MergeCustomObjectIntoOrderedDictionary -dst $res -src $settingsJson } catch { Write-PSFMessage -Level Host -Message "Settings file $settingsPath, is wrongly formatted." -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" return throw } } Write-PSFMessage -Level Verbose -Message "Settings file '$settingsFile' - processed" } Write-PSFMessage -Level Host -Message "Settings were updated succesfully." Invoke-TimeSignal -End } end{ } } <# .SYNOPSIS Installation of Nuget CLI .DESCRIPTION Download latest Nuget CLI .PARAMETER MetadataPath Path to the local Metadata folder .PARAMETER Url Url/Uri to zip file contains code/package/axmodel .PARAMETER FileName The name of the file should be downloaded by the url. Use if the url doesnt contain the filename. .EXAMPLE PS C:\> Update-FSCPSISVSource MetadataPath "C:\temp\PackagesLocalDirectory" -Url "https://ciellosarchive.blob.core.windows.net/test/Main-Extension-10.0.39_20240516.263.zip?sv=2023-01-03&st=2024-05-21T14%3A26%3A41Z&se=2034-05-22T14%3A26%3A00Z&sr=b&sp=r&sig=W%2FbS1bQrr59i%2FBSHWsftkfNsE1HvFXTrICwZSFiUItg%3D"" This will update the local metadata with the source from the downloaded zip archive. .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Update-FSCPSISVSource { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param ( [Parameter(HelpMessage = "The path to the metadata", Mandatory = $true)] [string] $MetadataPath, [Parameter(HelpMessage = "The url to the file contains the D365FSC axmodel/modelSourceCode/deployablePackage", Mandatory = $true)] [string] $Url, [Parameter(HelpMessage = "The name of the downloading file", Mandatory = $false)] [string] $FileName ) begin { try { if([string]::IsNullOrEmpty($FileName)) { $_tmpUrlPart = ([uri]$Url).Segments[-1] if($_tmpUrlPart.Contains(".")) { $FileName = $_tmpUrlPart } } if([string]::IsNullOrEmpty($FileName)) { throw "FileName is empty or cannot be parsed from the url. Please specify the FileName parameter." } if( (-not $FileName.Contains(".zip")) -and (-not $FileName.Contains(".axmodel")) ) { throw "Only a zip or axmodel file can be processed." } if(Test-Path "$($MetadataPath)/PackagesLocalDirectory") { $MetadataPath = (Join-Path $($MetadataPath) "/PackagesLocalDirectory") } elseif(Test-Path "$($MetadataPath)/Metadata") { $MetadataPath = (Join-Path $($MetadataPath) "/Metadata") } #$script:DefaultTempPath $tempPath = Join-Path -Path $script:DefaultTempPath -ChildPath "updateSource" #Cleanup existing temp folder Remove-Item -Path $tempPath -Recurse -Force -ErrorAction SilentlyContinue -Confirm:$false $downloadPath = Join-Path -Path $tempPath -ChildPath $fileName if (-not (Test-PathExists -Path $tempPath -Type Container -Create)) { return } } catch { Write-PSFMessage -Level Host -Message "Error: " -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } } process { if (Test-PSFFunctionInterrupt) { return } $helperPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\scripts\helpers.ps1" -Resolve . ($helperPath) try { Write-PSFMessage -Level Important -Message "Downloading $($FileName)" -Target $downloadPath [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 #[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true} Write-PSFMessage -Level Important -Message "Source: $Url" Write-PSFMessage -Level Important -Message "Destination $downloadPath" Start-BitsTransfer -Source $Url -Destination $downloadPath #check is archive contains few archives $packagesPaths = [System.Collections.ArrayList]@() $sourceCodePaths = [System.Collections.ArrayList]@() $axmodelsPaths = [System.Collections.ArrayList]@() if($downloadPath.EndsWith(".zip")) { Unblock-File $downloadPath Expand-7zipArchive -Path $downloadPath -DestinationPath "$tempPath/archives" $ispackage = Get-ChildItem -Path "$tempPath/archives" -Filter 'AXUpdateInstaller.exe' -ErrorAction SilentlyContinue -Force if($ispackage) { $null = $packagesPaths.Add($downloadPath) } else { Get-ChildItem "$tempPath/archives" -Filter '*.zip' -Recurse -ErrorAction SilentlyContinue -Force | ForEach-Object{ $archive = $_.FullName $tmpArchivePath = Join-Path "$tempPath/archives" $_.BaseName Unblock-File $archive Expand-7zipArchive -Path $archive -DestinationPath $tmpArchivePath $ispackage = Get-ChildItem -Path $tmpArchivePath -Filter 'AXUpdateInstaller.exe' -Recurse -ErrorAction SilentlyContinue -Force if($ispackage) { $null = $packagesPaths.Add($_.FullName) } else { if($_.FullName -notlike "*dynamicsax-*.zip") { $null = $sourceCodePaths.Add($_.FullName) } } } #check axmodel files inside and add to list if found Get-ChildItem "$tempPath/archives" -Filter '*.axmodel' -Recurse -ErrorAction SilentlyContinue -Force | ForEach-Object { $null = $axmodelsPaths.Add($_.FullName) } } } if($downloadPath.EndsWith(".axmodel")) { $null = $axmodelsPaths.Add($_.FullName) } foreach($package in $packagesPaths) { try { $package = Get-ChildItem $package Write-PSFMessage -Level Important -Message "The package $($package.BaseName) importing..." $tmpPackagePath = Join-Path "$tempPath/packages" $package.BaseName Unblock-File $package Expand-7zipArchive -Path $package -DestinationPath $tmpPackagePath $models = Get-ChildItem -Path $tmpPackagePath -Filter "dynamicsax-*.zip" -Recurse -ErrorAction SilentlyContinue -Force foreach($model in $models) { Write-PSFMessage -Level Important -Message "$($model.BaseName) processing..." $zipFile = [IO.Compression.ZipFile]::OpenRead($model.FullName) $zipFile.Entries | Where-Object {$_.FullName.Contains(".xref")} | ForEach-Object{ $modelName = $_.Name.Replace(".xref", "") $targetModelPath = (Join-Path $MetadataPath "$modelName/") if(Test-Path $targetModelPath) { Remove-Item $targetModelPath -Recurse -Force } Write-PSFMessage -Level Important -Message "'$($model.FullName)' to the $($targetModelPath)..." Expand-7zipArchive -Path $model.FullName -DestinationPath $targetModelPath } $zipFile.Dispose() } Write-PSFMessage -Level Important -Message "The package $($package) imported" } catch { Write-PSFMessage -Level Host -Message "Error:" -Exception $PSItem.Exception Write-PSFMessage -Level Important -Message "The package $($package) is not imported" } } if(($axmodelsPaths.Count -gt 0) -and ($PSVersionTable.PSVersion.Major -gt 5)) { Write-PSFMessage -Level Warning -Message "The axmodel cannot be imported. Current PS version is $($PSVersionTable.PSVersion). The latest PS major version acceptable to import the axmodel is 5." } else { $PlatformVersion = (Get-FSCPSVersionInfo -Version 10.0.38).data.PlatformVersion $nugetsPath = Join-Path $tempPath "NuGets" $compilerNugetPath = Join-Path $nugetsPath "Microsoft.Dynamics.AX.Platform.CompilerPackage.$PlatformVersion.nupkg" $compilerPath = Join-Path $tempPath "Microsoft.Dynamics.AX.Platform.CompilerPackage.$PlatformVersion" $null = Test-PathExists -Path $compilerPath -Type Container -Create $null = Test-PathExists -Path $nugetsPath -Type Container -Create Write-PSFMessage -Level Important -Message "The $PlatformVersion Platform Version used." Get-FSCPSNuget -Version $PlatformVersion -Type PlatformCompilerPackage -Path $nugetsPath Write-PSFMessage -Level Important -Message "The PlatformCompiler NuGet were downloaded at $nugetsPath." Expand-7zipArchive -Path $compilerNugetPath -DestinationPath $compilerPath $curLocation = Get-Location Set-Location $compilerPath try { $miscPath = Join-Path -Path $($Script:ModuleRoot) -ChildPath "\internal\misc" Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.Client.dll" -Destination $compilerPath -Force Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.Common.dll" -Destination $compilerPath -Force Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.Diff.dll" -Destination $compilerPath -Force Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.VersionControl.Client.dll" -Destination $compilerPath -Force Copy-Item -Path "$miscPath\Microsoft.TeamFoundation.VersionControl.Common.dll" -Destination $compilerPath -Force } catch { Write-PSFMessage -Level Important -Message $_.Exception.Message } foreach($axModel in $axmodelsPaths) { try { Write-PSFMessage -Level Important -Message "The axmodel $($axModel) importing..." Enable-D365Exception #Import-D365Model -Path $axModel -MetaDataDir $MetadataPath -BinDir $compilerPath -Replace Invoke-ModelUtil -Path $axModel -MetaDataDir $MetadataPath -BinDir $compilerPath -Command Replace Disable-D365Exception Write-PSFMessage -Level Important -Message "The axmodel $($axModel) imported." } catch { Disable-D365Exception Write-PSFMessage -Level Host -Message "Error:" -Exception $PSItem.Exception Write-PSFMessage -Level Important -Message "The axmodel $($axModel) is not imported." } } Set-Location $curLocation } foreach($sourceCode in $sourceCodePaths) { try { Write-PSFMessage -Level Important -Message "The source code $($sourceCode) importing..." $zipFile = [IO.Compression.ZipFile]::OpenRead($sourceCode) $zipFile.Entries | Where-Object {$_.FullName.Contains(".xref")} | ForEach-Object{ $modelName = $_.Name.Replace(".xref", "") $targetModelPath = (Join-Path $MetadataPath "$modelName/") Remove-Item $targetModelPath -Recurse -Force Expand-7zipArchive -Path $($sourceCode) -DestinationPath $targetModelPath } $zipFile.Dispose() Write-PSFMessage -Level Important -Message "The source code $($sourceCode) imported" } catch { Write-PSFMessage -Level Host -Message "Error:" -Exception $PSItem.Exception Write-PSFMessage -Level Important -Message "The source code $($sourceCode) is not imported" } } ## Cleanup XppMetadata Get-ChildItem -Path $MetadataPath -Directory -Filter "*XppMetadata" -Recurse | ForEach-Object { Remove-Item -Path $_.FullName -Recurse -Force } } catch { Write-PSFMessage -Level Host -Message "Error:" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } } end{ } } <# .SYNOPSIS This updates the D365FSC model version .DESCRIPTION This updates the D365FSC model version .PARAMETER xppSourcePath Path to the xpp metadata folder .PARAMETER xppDescriptorSearch Descriptor search pattern .PARAMETER xppLayer Layer of the code .PARAMETER versionNumber Target model version change to .EXAMPLE PS C:\> Update-FSCPSModelVersion -xppSourcePath "c:\temp\metadata" -xppLayer "ISV" -versionNumber "5.4.8.4" -xppDescriptorSearch $("TestModel"+"\Descriptor\*.xml") this will change the version of the TestModel to 5.4.8.4 .NOTES Author: Oleksandr Nikolaiev (@onikolaiev) #> function Update-FSCPSModelVersion { [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "")] [CmdletBinding()] param( [Parameter()] [string]$xppSourcePath, [Parameter()] [string]$xppDescriptorSearch, $xppLayer, $versionNumber ) begin{ Invoke-TimeSignal -Start Write-PSFMessage -Level Important -Message "xppSourcePath: $xppSourcePath" Write-PSFMessage -Level Important -Message "xppDescriptorSearch: $xppDescriptorSearch" Write-PSFMessage -Level Important -Message "xppLayer: $xppLayer" Write-PSFMessage -Level Important -Message "versionNumber: $versionNumber" if ($xppDescriptorSearch.Contains("`n")) { [string[]]$xppDescriptorSearch = $xppDescriptorSearch -split "`n" } $null = Test-Path -LiteralPath $xppSourcePath -PathType Container if ($versionNumber -match "^\d+\.\d+\.\d+\.\d+$") { $versions = $versionNumber.Split('.') } else { throw "Version Number '$versionNumber' is not of format #.#.#.#" } switch ( $xppLayer ) { "SYS" { $xppLayer = 0 } "SYP" { $xppLayer = 1 } "GLS" { $xppLayer = 2 } "GLP" { $xppLayer = 3 } "FPK" { $xppLayer = 4 } "FPP" { $xppLayer = 5 } "SLN" { $xppLayer = 6 } "SLP" { $xppLayer = 7 } "ISV" { $xppLayer = 8 } "ISP" { $xppLayer = 9 } "VAR" { $xppLayer = 10 } "VAP" { $xppLayer = 11 } "CUS" { $xppLayer = 12 } "CUP" { $xppLayer = 13 } "USR" { $xppLayer = 14 } "USP" { $xppLayer = 15 } } } process{ # Discover packages #$BuildModuleDirectories = @(Get-ChildItem -Path $BuildMetadataDir -Directory) #foreach ($BuildModuleDirectory in $BuildModuleDirectories) #{ $potentialDescriptors = Find-FSCPSMatch -DefaultRoot $xppSourcePath -Pattern $xppDescriptorSearch | Where-Object { (Test-Path -LiteralPath $_ -PathType Leaf) } if ($potentialDescriptors.Length -gt 0) { Write-PSFMessage -Level Verbose -Message "Found $($potentialDescriptors.Length) potential descriptors" foreach ($descriptorFile in $potentialDescriptors) { try { [xml]$xml = Get-Content $descriptorFile -Encoding UTF8 $modelInfo = $xml.SelectNodes("/AxModelInfo") if ($modelInfo.Count -eq 1) { $layer = $xml.SelectNodes("/AxModelInfo/Layer")[0] $layerid = $layer.InnerText $layerid = [int]$layerid $modelName = ($xml.SelectNodes("/AxModelInfo/Name")).InnerText # If this model's layer is equal or above lowest layer specified if ($layerid -ge $xppLayer) { $version = $xml.SelectNodes("/AxModelInfo/VersionMajor")[0] $version.InnerText = $versions[0] $version = $xml.SelectNodes("/AxModelInfo/VersionMinor")[0] $version.InnerText = $versions[1] $version = $xml.SelectNodes("/AxModelInfo/VersionBuild")[0] $version.InnerText = $versions[2] $version = $xml.SelectNodes("/AxModelInfo/VersionRevision")[0] $version.InnerText = $versions[3] $xml.Save($descriptorFile) Write-PSFMessage -Level Verbose -Message " - Updated model $modelName version to $versionNumber in $descriptorFile" } else { Write-PSFMessage -Level Verbose -Message " - Skipped $modelName because it is in a lower layer in $descriptorFile" } } else { Write-PSFMessage -Level Error -Message "File '$descriptorFile' is not a valid descriptor file" } } catch { Write-PSFMessage -Level Host -Message "Something went wrong while updating D365FSC package versiob" -Exception $PSItem.Exception Stop-PSFFunction -Message "Stopping because of errors" -EnableException $true return } finally{ } } } #} } end{ Invoke-TimeSignal -End } } |