Functions/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 'BC170Test' .Example Publish-BcAddin ` -Path @('C:\dotnet\PDF On-Premises', 'C:\dotnet\SharePoint On-Premises') ` -Destination 'C:\Program Files\Microsoft Dynamics 365 Business Central\160\Service\Add-ins' .Example Publish-BcAddin -Path 'c:\release\dotnet' -BcVersion 'bc15' #> 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') ) " _____ _ _ _ _ _ _ _ _ _ _ | __ \ | | | (_) | | | \ | | | | /\ | | | (_) | |__) | _| |__ | |_ ___| |__ | \| | ___| |_ / \ __| | __| |_ _ __ ___ | ___/ | | | '_ \| | / __| '_ \ | . `` |/ _ \ __| / /\ \ / _`` |/ _`` | | '_ \/ __| | | | |_| | |_) | | \__ \ | | | _| |\ | __/ |_ / ____ \ (_| | (_| | | | | \__ \ |_| \__,_|_.__/|_|_|___/_| |_| (_)_| \_|\___|\__| /_/ \_\__,_|\__,_|_|_| |_|___/ 4PS v{0} " -f $MyInvocation.MyCommand.Module.Version | Write-Host # Validate PowerShell version if (-not $PSVersionTable.PSVersion.Major -ge '5') { $msg = 'Powershell 5.0 or higher required to continue. Current version is {0}.{1}' -f ` $PSVersionTable.PSVersion.Major, $PSVersionTable.PSVersion.Minor throw $msg } 'PowerShell version {0} is compatible.' -f $PSVersionTable.PSVersion.ToString() | Write-Host # Validate if PowerShell is started as administrator $windowsIdentity = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()) $elevated = ($windowsIdentity.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) if (-not $elevated) { $msg = 'You need administrative privileges to deploy Business Central add-ins. ' $msg += 'Start the script in a Powershell session launched as administrator.' throw $msg } 'Script is executed as administrator.' | Write-Host # Validate if script is executed in a 64 bit process. if(-not [Environment]::Is64BitProcess){ $msg = 'This script needs to be executed as a 64 bit process. Current process is x86 (32bit).' throw $msg } 'Session is 64 bit.' | Write-Host try{ Stop-Transcript }catch{} # Create logfile $logFile = Join-Path -Path $LogFilePath -ChildPath ('{0}_fps.log' -f (Get-Date).ToString('yyyy-MM-dd_HH.mm.ss')) New-Item -Path $logFile -ItemType File -Force | Out-Null # Remove old logfiles (keep max 10 logfiles) $LogFiles = Get-ChildItem $LogFilePath -File -Filter '*_fps.log' if($LogFiles.Count -gt 10){ $Exclude = $LogFiles | Sort-Object -Property CreationTime -Descending | Select-Object -First 10 Get-ChildItem (Split-Path $logFile -Parent) -Exclude $Exclude | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue } Start-Transcript -path $logFile -append ### Start script if(!$Destination){ '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 'BC17', 'BC13', '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' } else { 'Supplied destination: ''{0}''.' -f $Destination | Write-Host if((Split-Path $Destination -Leaf) -ne '_4PS'){ $Destination = Join-Path $Destination '_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){ # 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) } # If the folder exists, remove it first. if((Test-Path $target) -eq $true){ if ($Force){ 'The destination folder ''{0}'' already exists. Removing the folder..' -f $target | Write-Host # TOD: Try Catch on locked item, if lock ask to stop all BC services. Remove-Item -Path $target -Recurse -Force } else { $msg = 'The destination folder ''{0}'' already exists. Please remove the folder first or use the -Force parameter to replace the folder.' -f $target Write-Warning $msg continue } } "`nCopying content from: `n'{0}' to `n'{1}'" -f $dir, $target | Write-Host Copy-Item -Path $dir -Destination $target -Recurse -Force # 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 |