Functions/PlatformManagement/Publish-BcAddin.ps1
<#
.Synopsis Installs DotNet Assemblies into the Microsoft Dynamics 365 Business Central Addin folder. .Description The parameter Path is the source path of all dotnet addins. The target folder (root add-in folder) can be supplied directly with param Destination, or can be received dynamically based on the BC version or a Business Central Service Tier. .Example Publish-BcAddin -Path 'c:\release\dotnet\add-in' -ServerInstance 'BC210Test' .Example Publish-BcAddin ` -Path @('C:\dotnet\PDF On-Premises', 'C:\dotnet\SharePoint On-Premises') ` -Destination 'C:\Program Files\Microsoft Dynamics 365 Business Central\210\Service\Add-ins' .Example Publish-BcAddin -Path 'c:\release\dotnet' -BcVersion 'bc21' #> function Publish-BcAddin { [CmdletBinding(DefaultParameterSetName='ServerInstance')] param( # The path to the folder(s) to publish to the add-in folder. # E.g. 'C:\dotnet\PDF On-Premises' or @('C:\dotnet\PDF On-Premises', 'C:\dotnet\SharePoint On-Premises') [Parameter(Mandatory = $true)] [string[]] $Path, # Publishes the assemblies into the supplied BC addinfolder. # E.g. 'C:\Program Files\Microsoft Dynamics 365 Business Central\160\Service\Add-ins' [Parameter(ParameterSetName='Destination', Mandatory = $true)] [string] $Destination, # Publishes the assemblies into the BC addinfolder compatible with the supplied serverinstance. # E.g. 'BC170' [Parameter(ParameterSetName='ServerInstance', Mandatory = $true)] [string] $ServerInstance, # Publishes the assemblies into the BC addinfolder compatible with the supplied BcVersion. # E.g. 'bc17' or 'bc15'. [Parameter(ParameterSetName='BcVersion', Mandatory = $true)] [String] $BcVersion, # Location to write the logfile to. Default location is '?:\ProgramData\4ps\dotnetdeployment'. # E.g. 'C:\BcInstallation\Log' [string] $LogFilePath = (Join-Path -Path $env:ProgramData -ChildPath '4ps\dotnetdeployment'), # When enabled and when the target folder already exists than the target folder will be removed first. [switch] $Force ) " _____ _ _ _ _ _ _ _ _ _ _ | __ \ | | | (_) | | | \ | | | | /\ | | | (_) | |__) | _| |__ | |_ ___| |__ | \| | ___| |_ / \ __| | __| |_ _ __ ___ | ___/ | | | '_ \| | / __| '_ \ | . `` |/ _ \ __| / /\ \ / _`` |/ _`` | | '_ \/ __| | | | |_| | |_) | | \__ \ | | | _| |\ | __/ |_ / ____ \ (_| | (_| | | | | \__ \ |_| \__,_|_.__/|_|_|___/_| |_| (_)_| \_|\___|\__| /_/ \_\__,_|\__,_|_|_| |_|___/ 4PS v{0} " -f $MyInvocation.MyCommand.Module.Version | Write-Host Test-Preconditions -Is64Bit -HasElevatedRights -ValidPowerShellVersion -HasFullLanguage -MinimalMajorVersion 5 -MinimalMinorVersion 0 -Verbose -ErrorAction Stop try{ Stop-Transcript }catch{} # Validate if module FpsGeneral is available on which BcDeployment depends if(-not (Get-Module FpsGeneral)){ if(Get-Module -ListAvailable FpsGeneral){ Import-Module FpsGeneral -DisableNameChecking -Force } else{ Write-Error 'Please install and import PowerShell module FpsGeneral before executing this cmdlet.' return } } # Create logfile $LogFile = New-FpsLogFile -LogFilePath $LogFilePath -LogFileNameSuffix 'fps' -LogFilesToPreserve 10 Start-Transcript -path $LogFile -append ### Start script if($Destination){ 'Supplied destination: ''{0}''.' -f $Destination | Write-Host if((Split-Path $Destination -Leaf) -ne '_4PS'){ $Destination = Join-Path $Destination '_4PS' } } else{ 'Destination is not set. Retreiving destination dynamically...' | Write-Host if($BcVersion){ if((Get-BcVersion -BcVersion $BcVersion) -eq $false){ "The supplied BC/NAV version '{0}' is not valid. Valid values are for example 'BC21', 'BC19', 'NAV2017'" -f $BcVersion | Write-Warning return } [hashtable] $BcVersion = Get-BcVersion -BcVersion $BcVersion } if($ServerInstance){ $BcVersionFolder = ([version](Get-BCServerInstance -ServerInstance $ServerInstance).Version).Major * 10 [hashtable] $BcVersion = Get-BcVersion -VersionFolder $BcVersionFolder } # Get installation folder $Service = Get-BcComponent -BcVersion $BcVersion.Version | Where-Object Component -eq 'NST' if (-not $Service.IsInstalled) { Write-Warning 'Cannot find BC add-in folder because the BC Service folder is not found. Make sure the BC Service is installed.' return $false } $Destination = Join-Path $Service.InstallLocation 'Add-ins\_4PS' } 'Destination is set to ''{0}''.' -f $Destination | Write-Host # Test if source and target folder exists $Path | ForEach-Object { if((Test-Path $_) -eq $false){ $msg = 'Cannot find assembly path. Please check the path ''{0}''' -f $_ Write-Error $msg } else { 'Source path ''{0}'' exists.' -f $_ | Write-Host } } if((Test-Path (Split-Path $Destination -Parent)) -eq $false){ $msg = 'Cannot find the destination path. Please check the path ''{0}''' -f (Split-Path $Destination -Parent) Write-Error $msg } else { 'Root add-in directory ''{0}'' exists.' -f (Split-Path $Destination -Parent) | Write-Host } if((Test-Path $Destination) -eq $false){ 'Folder ''{0}'' doesn''s exist in root add-in folder. Creating folder..' -f (Split-Path $Destination -Leaf) | Write-Host New-Item $Destination -ItemType Directory -Force } else { 'Destination ''{0}'' exists.' -f $Destination | Write-Host } # Copy source folders to destination foreach($dir in $Path){ "`nUpdating components {0}..." -f ($dir | Split-Path -Leaf) # If the folder starts with a number, add a '_' prefix. if((Split-Path $dir -Leaf) -match '^[0-9]+.*'){ $target = Join-Path $Destination ('_{0}' -f (Split-Path $dir -Leaf)) } else { $target = Join-Path $Destination (Split-Path $dir -Leaf) } $filesToCopy = Get-ChildItem -Path $dir -Recurse $lockedFiles = @() # If the folder exists it should be removed unless the source and target are the same. if((Test-Path $target) -eq $true){ if(-not $Force){ $msg = 'The destination folder ''{0}'' already exists. ' -f $target $msg += 'Please remove the folder first or use the -Force parameter to replace the folder.' $msg | Write-Warning continue } 'The destination folder ''{0}'' already exists.' -f $target | Write-Host # Compare hashes of source and destination directory $sourceHashes = Get-DirectoryHash -Path $dir $targetHashes = Get-DirectoryHash -Path $target if (-not (Compare-Object -ReferenceObject $sourceHashes -DifferenceObject $targetHashes -SyncWindow 0)) { 'Source and destination folders are identical, skipping.' | Write-Host continue } 'Files in the source and destination are not the same, removing the destination folder.' | Write-Host Remove-Item -Path $target -Recurse -Force -ErrorAction SilentlyContinue } "Copying files from: `n '{0}' to `n '{1}'" -f $dir, $target | Write-Host foreach ($file in $filesToCopy) { # Calculate relative path $relativePath = $file.FullName.Substring($dir.Length) # Create full destination path $destinationPath = Join-Path $target $relativePath # Ensure that the destination directory exists $destinationDir = Split-Path -Path $destinationPath -Parent if (!(Test-Path $destinationDir)) { $null = New-Item -ItemType Directory -Path $destinationDir -ErrorAction Continue | Out-Null } try { Copy-Item -Path $file.FullName -Destination $destinationPath -ErrorAction Stop } catch { # Assume any errors are due to the file being locked $lockedFiles += $destinationPath } } if ($lockedFiles) { $message = "The following files could not be copied to the destination: `n{0}" -f $($lockedFiles -join ', ') $message += "`nCheck if the files are still locked. Unluck the files and try again." Write-Error $message } else { 'Done' | Write-Host } # If the add-in folder contains .zip files, extract the archives. $archives = Get-ChildItem -Path $target -Recurse -Filter '*.zip' if($archives.Count -ge 1){ 'There are {0} .zip archives found in the folder ''{1}''. Extracting the content...' -f $archives.Count, (Split-Path $target -Leaf) | Write-Host } $archives | ForEach-Object { $archiveTarget = Join-Path $target ([io.path]::GetFileNameWithoutExtension($_)) if((Test-Path $archiveTarget) -eq $false){ New-Item $archiveTarget -ItemType Directory -Force } if ((Get-Command Expand-Archive).Source -eq 'Pscx'){ Expand-Archive -Path $_.FullName -OutputPath $archiveTarget -Force } else { Expand-Archive -Path $_.FullName -DestinationPath $archiveTarget -Force } Remove-Item $_.FullName -Force } } 'Installation completed.' | Write-Host try{ Stop-Transcript }catch{} } Export-ModuleMember -Function Publish-BcAddin function Get-DirectoryHash($Path){ $hashes = Get-ChildItem -Path $Path -File -Recurse | Get-FileHash | Select-Object -ExpandProperty Hash $sortedHashes = $hashes | Sort-Object return $sortedHashes } |