From 6a7abb5a255c06e3854b9aef9205352007e62ba8 Mon Sep 17 00:00:00 2001 From: Gioxx Date: Wed, 1 Oct 2025 14:27:23 +0200 Subject: [PATCH] 1.0.4 - Bugfix: Removed any reference to ZIP uploads as setup files. - Bugfix: Fixed PS 5.1 incompatibility in relative-path resolution ([System.IO.Path]::GetRelativePath is PS 7+). - Improved: Code cleanup, removed redundant GitHub download logic; refactoring. - Improved: Validates setup file existence and type. - Improved: Tries to create output folder when missing. - Improved: Ensures exactly one ".intunewin" extension on output. - Improved: If Source folder is not specified, it is inferred from Setup file. - Improved: Added more inline comments for maintainability. --- Assets/Intune.png | Bin 0 -> 29523 bytes IntuneWinAppUtilGUI.psd1 | 49 +-- IntuneWinAppUtilGUI.psm1 | 44 ++- Private/Get-ExeVersion.ps1 | 28 ++ Private/Get-RelativePath.ps1 | 50 +++ Private/Initialize-IntuneWinAppUtil.ps1 | 33 ++ Private/Invoke-DownloadIntuneTool.ps1 | 89 ++++++ Private/Set-SetupFromSource.ps1 | 71 +++++ Private/Show-ToolVersion.ps1 | 22 ++ Public/Show-IntuneWinAppUtilGui.ps1 | 389 ++++++++++-------------- README.md | 4 +- UI/UI.xaml | 259 ++++++++-------- 12 files changed, 663 insertions(+), 375 deletions(-) create mode 100644 Assets/Intune.png create mode 100644 Private/Get-ExeVersion.ps1 create mode 100644 Private/Get-RelativePath.ps1 create mode 100644 Private/Initialize-IntuneWinAppUtil.ps1 create mode 100644 Private/Invoke-DownloadIntuneTool.ps1 create mode 100644 Private/Set-SetupFromSource.ps1 create mode 100644 Private/Show-ToolVersion.ps1 diff --git a/Assets/Intune.png b/Assets/Intune.png new file mode 100644 index 0000000000000000000000000000000000000000..5f09e7435ccdc3e8f7d6c2cead0a7674ce5fc4a8 GIT binary patch literal 29523 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 I)O6JRgt&5ars9jC%Rw7|U+v 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 mQb-|%6jDebg%onV + + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Path + ) + + try { + if (-not (Test-Path $Path)) { return $null } + $vi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($Path) + if ($vi.FileVersion -and $vi.FileVersion.Trim()) { return $vi.FileVersion.Trim() } + if ($vi.ProductVersion -and $vi.ProductVersion.Trim()) { return $vi.ProductVersion.Trim() } + return $null + } catch { + return $null + } +} diff --git a/Private/Get-RelativePath.ps1 b/Private/Get-RelativePath.ps1 new file mode 100644 index 0000000..6fc6d41 --- /dev/null +++ b/Private/Get-RelativePath.ps1 @@ -0,0 +1,50 @@ +function Get-RelativePath { + <# + .SYNOPSIS + Returns a relative Windows path from BasePath to TargetPath when possible; + otherwise returns the absolute, normalized TargetPath. + .DESCRIPTION + - Normalizes base and target paths via [System.IO.Path]::GetFullPath. + - If paths are on different roots (drive letters or UNC shares), falls back to absolute. + - Uses Uri.MakeRelativeUri to compute the relative portion. + - Decodes URL-encoded characters and converts forward slashes to backslashes. + .PARAMETER BasePath + The base directory you want to compute the relative path from. + .PARAMETER TargetPath + The file or directory path you want to compute the relative path to. + .OUTPUTS + [string] Relative path if computable, otherwise absolute normalized TargetPath. + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$BasePath, + [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$TargetPath + ) + + try { + # Normalize and ensure BasePath ends with a directory separator so Uri treats it as a folder + $baseFull = [System.IO.Path]::GetFullPath(($BasePath.TrimEnd('\') + '\')) + $targetFull = [System.IO.Path]::GetFullPath($TargetPath) + + # If roots differ (e.g., C:\ vs D:\ or different UNC shares), relative path is not possible + $baseRoot = [System.IO.Path]::GetPathRoot($baseFull) + $targetRoot = [System.IO.Path]::GetPathRoot($targetFull) + if (-not [string]::Equals($baseRoot, $targetRoot, [System.StringComparison]::OrdinalIgnoreCase)) { + return $targetFull + } + + # Compute the relative URI and convert it to a Windows path + $uriBase = [Uri]$baseFull + $uriTarget = [Uri]$targetFull + + $rel = $uriBase.MakeRelativeUri($uriTarget).ToString() + # Decode URL-encoded chars (e.g., spaces) and switch to backslashes + $relWin = [Uri]::UnescapeDataString($rel).Replace('/', '\') + + return $relWin + } catch { + # On any unexpected error, just return the original target (best-effort behavior) + return $TargetPath + } +} \ No newline at end of file diff --git a/Private/Initialize-IntuneWinAppUtil.ps1 b/Private/Initialize-IntuneWinAppUtil.ps1 new file mode 100644 index 0000000..4d209ed --- /dev/null +++ b/Private/Initialize-IntuneWinAppUtil.ps1 @@ -0,0 +1,33 @@ +function Initialize-IntuneWinAppUtil { + <# + .SYNOPSIS + Returns a valid IntuneWinAppUtil.exe path or $null on failure. + .DESCRIPTION + - If a UI-provided path is valid, use it. + - Else, use cached copy under %APPDATA%\IntuneWinAppUtilGUI\bin. + - Else, download the latest via Invoke-DownloadIntuneTool (private helper). + .PARAMETER UiToolPath + Optional path provided by the UI (textbox). + .OUTPUTS + [string] or $null + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory = $false)][string]$UiToolPath + ) + + try { + $appRoot = Join-Path $env:APPDATA 'IntuneWinAppUtilGUI' + $binDir = Join-Path $appRoot 'bin' + $exePath = Join-Path $binDir 'IntuneWinAppUtil.exe' + + if (-not [string]::IsNullOrWhiteSpace($UiToolPath) -and (Test-Path $UiToolPath)) { return $UiToolPath } + if (Test-Path $exePath) { return $exePath } + + # Fallback: download latest tool + return (Invoke-DownloadIntuneTool) + } catch { + return $null + } +} diff --git a/Private/Invoke-DownloadIntuneTool.ps1 b/Private/Invoke-DownloadIntuneTool.ps1 new file mode 100644 index 0000000..7906df1 --- /dev/null +++ b/Private/Invoke-DownloadIntuneTool.ps1 @@ -0,0 +1,89 @@ +function Invoke-DownloadIntuneTool { + <# + .SYNOPSIS + Downloads the latest IntuneWinAppUtil.exe from GitHub and caches it under + %APPDATA%\IntuneWinAppUtilGUI\bin. + .DESCRIPTION + - Forces TLS 1.2 for GitHub downloads. + - Creates (or ensures) the bin directory under $env:APPDATA\IntuneWinAppUtilGUI\bin. + - Removes any stale IntuneWinAppUtil.exe in the bin directory. + - Downloads the repository master ZIP, extracts it to a unique temp folder, + locates IntuneWinAppUtil.exe, and copies it into the bin directory. + - Cleans up all temp files/folders in a finally block. + - Returns the full path to the cached IntuneWinAppUtil.exe. + - Throws on failure (caller can catch and show a message box). + .PARAMETER DestinationRoot + Optional base path for the cache (default: $env:APPDATA\IntuneWinAppUtilGUI). + .PARAMETER RepoZipUrl + Optional ZIP URL (default: master branch of Microsoft-Win32-Content-Prep-Tool). + .OUTPUTS + [string] Full path to IntuneWinAppUtil.exe. + .EXAMPLE + $exe = Invoke-DownloadIntuneTool + # $exe now points to %APPDATA%\IntuneWinAppUtilGUI\bin\IntuneWinAppUtil.exe + #> + + [CmdletBinding()] + param( + [string]$DestinationRoot = (Join-Path $env:APPDATA 'IntuneWinAppUtilGUI'), + [string]$RepoZipUrl = 'https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/archive/refs/heads/master.zip' + ) + + # Ensure TLS 1.2 for GitHub + try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {} + + $binDir = Join-Path $DestinationRoot 'bin' + $exePath = Join-Path $binDir 'IntuneWinAppUtil.exe' + $tempZip = Join-Path $env:TEMP ("IntuneWinAppUtil-{0}.zip" -f ([guid]::NewGuid())) + $tempDir = Join-Path $env:TEMP ("IntuneExtract-{0}" -f ([guid]::NewGuid())) + + try { + # Prepare target folder and clean stale exe + New-Item -ItemType Directory -Path $binDir -Force | Out-Null + if (Test-Path $exePath) { Remove-Item $exePath -Force -ErrorAction SilentlyContinue } + + # Download + Invoke-WebRequest -Uri $RepoZipUrl -OutFile $tempZip -UseBasicParsing -ErrorAction Stop + + # Extract (fallback if overwrite overload isn't available) + New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + $zipType = [System.IO.Compression.ZipFile] + $hasOverwrite = $zipType.GetMethod( + 'ExtractToDirectory', + [Reflection.BindingFlags]'Public, Static', + $null, + @([string], [string], [bool]), + $null + ) + if ($hasOverwrite) { + [System.IO.Compression.ZipFile]::ExtractToDirectory($tempZip, $tempDir, $true) + } else { + [System.IO.Compression.ZipFile]::ExtractToDirectory($tempZip, $tempDir) + } + + # Locate exe + $found = Get-ChildItem -Path $tempDir -Recurse -Filter 'IntuneWinAppUtil.exe' -File -ErrorAction SilentlyContinue | Select-Object -First 1 + if (-not $found) { + throw "IntuneWinAppUtil.exe not found in the extracted archive." + } + + # Copy into cache + Copy-Item -Path $found.FullName -Destination $exePath -Force + return $exePath + } catch { + throw $_ + } finally { + # Best-effort cleanup + foreach ($p in @($tempZip, $tempDir)) { + try { + if (Test-Path $p) { + if (Test-Path $p -PathType Container) { + Remove-Item $p -Recurse -Force -ErrorAction SilentlyContinue + } else { + Remove-Item $p -Force -ErrorAction SilentlyContinue + } + } + } catch {} + } + } +} diff --git a/Private/Set-SetupFromSource.ps1 b/Private/Set-SetupFromSource.ps1 new file mode 100644 index 0000000..ac28ae7 --- /dev/null +++ b/Private/Set-SetupFromSource.ps1 @@ -0,0 +1,71 @@ +function Set-SetupFromSource { + <# + .SYNOPSIS + Suggests the setup file and (optionally) proposes the final package name from a given source folder. + .DESCRIPTION + - Recursively searches for 'Invoke-AppDeployToolkit.exe' under SourcePath. + - If found, populates the provided TextBox control (SetupFileControl) with a relative path + (via Get-RelativePath) when the exe resides under SourcePath. + - Does not overwrite SetupFileControl if it already points to an existing file (absolute + or relative to SourcePath). + - If 'Invoke-AppDeployToolkit.ps1' exists in the same folder, extracts AppName/AppVersion + and sets FinalFilenameControl.Text to 'AppName_Version' (sanitizing spaces and invalid + filename characters). + - Fails silently on parsing/IO errors. + .PARAMETER SourcePath + The source directory to inspect. Must exist. + .PARAMETER SetupFileControl + The TextBox to populate with the suggested setup path (relative when possible). + .PARAMETER FinalFilenameControl + The TextBox to populate with the proposed final filename (e.g., 'AppName_Version'). + .OUTPUTS + None. Mutates the provided TextBox controls. + .EXAMPLE + Set-SetupFromSource -SourcePath $SourceFolder.Text -SetupFileControl $SetupFile -FinalFilenameControl $FinalFilename + #> + + [CmdletBinding()] + param( + [Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$SourcePath, + [Parameter(Mandatory)][ValidateNotNull()][System.Windows.Controls.TextBox]$SetupFileControl, + [Parameter(Mandatory)][ValidateNotNull()][System.Windows.Controls.TextBox]$FinalFilenameControl + ) + + if (-not (Test-Path $SourcePath)) { return } + + # If current SetupFile value already points to an existing file (absolute or relative to source), do not override. + $current = $SetupFileControl.Text.Trim() + if ($current) { + if (Test-Path $current) { return } + $maybeRelative = Join-Path $SourcePath $current + if (Test-Path $maybeRelative) { return } + } + + # Search for Invoke-AppDeployToolkit.exe + $exeHit = Get-ChildItem -Path $SourcePath -Filter 'Invoke-AppDeployToolkit.exe' -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 + if ($exeHit) { + # Prefer a relative path when the file is inside the source folder + $SetupFileControl.Text = Get-RelativePath -BasePath $SourcePath -TargetPath $exeHit.FullName + + # Look for Invoke-AppDeployToolkit.ps1 in the same folder + $ps1Path = Join-Path $exeHit.Directory.FullName 'Invoke-AppDeployToolkit.ps1' + if (Test-Path $ps1Path) { + try { + $content = Get-Content $ps1Path -Raw + $appName = $null + $appVersion = $null + if ($content -match "AppName\s*=\s*'([^']+)'") { $appName = $matches[1] } + if ($content -match "AppVersion\s*=\s*'([^']+)'") { $appVersion = $matches[1] } + + if ($appName -and $appVersion) { + # Clean filename: remove spaces and invalid chars + $cleanName = ($appName -replace '\s+', '' -replace '[\\/:*?"<>|]', '-') + $cleanVer = ($appVersion -replace '\s+', '' -replace '[\\/:*?"<>|]', '-') + $FinalFilenameControl.Text = "${cleanName}_${cleanVer}" + } + } catch { + # fail silently + } + } + } +} \ No newline at end of file diff --git a/Private/Show-ToolVersion.ps1 b/Private/Show-ToolVersion.ps1 new file mode 100644 index 0000000..89e130d --- /dev/null +++ b/Private/Show-ToolVersion.ps1 @@ -0,0 +1,22 @@ +function Show-ToolVersion { + <# + .SYNOPSIS + Updates the provided TextBlock with IntuneWinAppUtil version text. + .PARAMETER Path + Full path to IntuneWinAppUtil.exe (can be $null/empty). + .PARAMETER Target + WPF TextBlock (or any object with a 'Text' property) to update. + #> + + param( + [string]$Path, + [Parameter(Mandatory)][object]$Target + ) + + $ver = if ($Path) { Get-ExeVersion -Path $Path } else { $null } + $Target.Text = if ($ver) { + "IntuneWinAppUtil version: $ver" + } else { + "IntuneWinAppUtil version: (not detected)" + } +} \ No newline at end of file diff --git a/Public/Show-IntuneWinAppUtilGui.ps1 b/Public/Show-IntuneWinAppUtilGui.ps1 index d1874fa..980e7cc 100644 --- a/Public/Show-IntuneWinAppUtilGui.ps1 +++ b/Public/Show-IntuneWinAppUtilGui.ps1 @@ -1,151 +1,14 @@ -# Show-Gui.ps1 -Add-Type -AssemblyName PresentationFramework -Add-Type -AssemblyName System.Windows.Forms -Add-Type -AssemblyName System.IO.Compression.FileSystem - -# Returns a relative path from BasePath to TargetPath when possible; otherwise returns the absolute path. -function Get-RelativePath { - param( - [Parameter(Mandatory)] [string]$BasePath, - [Parameter(Mandatory)] [string]$TargetPath - ) - - try { - $baseFull = [System.IO.Path]::GetFullPath(($BasePath.TrimEnd('\') + '\')) - $targetFull = [System.IO.Path]::GetFullPath($TargetPath) - $uriBase = [Uri]$baseFull - $uriTarget = [Uri]$targetFull - return $uriBase.MakeRelativeUri($uriTarget).ToString().Replace('/','\') - } catch { - return $TargetPath - } -} - -# If Invoke-AppDeployToolkit.exe exists under SourcePath, suggest it into the Setup textbox -# and optionally populate FinalFilename if AppName/AppVersion are found in Invoke-AppDeployToolkit.ps1 -function Set-SetupFromSource { - param([string]$SourcePath) - - if ([string]::IsNullOrWhiteSpace($SourcePath) -or -not (Test-Path $SourcePath)) { return } - - # If current SetupFile value already points to an existing file (absolute or relative to source), do not override. - $current = $SetupFile.Text.Trim() - if ($current) { - if (Test-Path $current) { return } - $maybeRelative = Join-Path $SourcePath $current - if (Test-Path $maybeRelative) { return } - } - - # Search for Invoke-AppDeployToolkit.exe - $exeHit = Get-ChildItem -Path $SourcePath -Filter 'Invoke-AppDeployToolkit.exe' -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1 - if ($exeHit) { - # Prefer a relative path when the file is inside the source folder - $relativeExe = Get-RelativePath -BasePath $SourcePath -TargetPath $exeHit.FullName - $SetupFile.Text = $relativeExe - - # Look for Invoke-AppDeployToolkit.ps1 in the same folder - $ps1Path = Join-Path $exeHit.Directory.FullName 'Invoke-AppDeployToolkit.ps1' - if (Test-Path $ps1Path) { - try { - $content = Get-Content $ps1Path -Raw - - $appName = if ($content -match "AppName\s*=\s*'([^']+)'") { $matches[1] } else { $null } - $appVersion = if ($content -match "AppVersion\s*=\s*'([^']+)'") { $matches[1] } else { $null } - - if ($appName -and $appVersion) { - # Clean filename: remove spaces and invalid chars - $cleanName = ($appName -replace '\s+', '' -replace '[\\/:*?"<>|]', '-') - $cleanVer = ($appVersion -replace '\s+', '' -replace '[\\/:*?"<>|]', '-') - $FinalFilename.Text = "${cleanName}_${cleanVer}" - } - } catch { - # Fail silently if parsing goes wrong - } - } - } -} - -# Returns file version (FileVersion preferred, then ProductVersion); $null if not available. -function Get-ExeVersion { - param([Parameter(Mandatory)][string]$Path) - - try { - if (-not (Test-Path $Path)) { return $null } - $vi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($Path) - if ($vi.FileVersion -and $vi.FileVersion.Trim()) { return $vi.FileVersion.Trim() } - if ($vi.ProductVersion -and $vi.ProductVersion.Trim()) { return $vi.ProductVersion.Trim() } - return $null - } catch { - return $null - } -} - -# Updates the ToolVersion TextBlock with current version or a default message. -function Show-ToolVersion { - param([string]$Path) - - if (-not $ToolVersionText) { return } - $ver = if ($Path) { Get-ExeVersion -Path $Path } else { $null } - $ToolVersionText.Text = if ($ver) { - "IntuneWinAppUtil version: $ver" - } else { - "IntuneWinAppUtil version: (not detected)" - } -} - -# Downloads the latest IntuneWinAppUtil.exe by fetching the master zip from GitHub, -# extracting it, locating the EXE, and copying it into %APPDATA%\IntuneWinAppUtilGUI\bin. -function Invoke-RedownloadIntuneTool { - param() - - $appRoot = Join-Path $env:APPDATA 'IntuneWinAppUtilGUI' - $binDir = Join-Path $appRoot 'bin' - $exePath = Join-Path $binDir 'IntuneWinAppUtil.exe' - $zipUrl = 'https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/archive/refs/heads/master.zip' - $zipPath = Join-Path $env:TEMP 'IntuneWinAppUtil-master.zip' - $extractTo = Join-Path $env:TEMP 'IntuneExtract' - - try { - # Clean previous temp - if (Test-Path $extractTo) { Remove-Item $extractTo -Recurse -Force } - - # Ensure bin dir exists (and clear old exe to avoid stale versions) - New-Item -ItemType Directory -Path $binDir -Force | Out-Null - if (Test-Path $exePath) { Remove-Item $exePath -Force -ErrorAction SilentlyContinue } - - # Download ZIP - Invoke-WebRequest -Uri $zipUrl -OutFile $zipPath -UseBasicParsing - - # Extract ZIP - [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $extractTo, $true) - - # Find EXE in extracted content - $found = Get-ChildItem -Path $extractTo -Recurse -Filter 'IntuneWinAppUtil.exe' -File -ErrorAction SilentlyContinue | Select-Object -First 1 - if (-not $found) { - throw "IntuneWinAppUtil.exe not found in extracted archive." - } - - # Copy to bin - Copy-Item -Path $found.FullName -Destination $exePath -Force - - # Cleanup temp - if (Test-Path $zipPath) { Remove-Item $zipPath -Force } - if (Test-Path $extractTo) { Remove-Item $extractTo -Recurse -Force } - - return $exePath - } catch { - throw $_ - } -} - +# Show-IntuneWinAppUtilGui.ps1 # Show the main GUI window and handle all events. function Show-IntuneWinAppUtilGui { [CmdletBinding()] param () + $moduleRoot = Split-Path -Path $PSScriptRoot -Parent $configPath = Join-Path -Path $env:APPDATA -ChildPath "IntuneWinAppUtilGUI\config.json" - $xamlPath = Join-Path -Path $PSScriptRoot -ChildPath "..\UI\UI.xaml" - $iconPath = Join-Path -Path $PSScriptRoot -ChildPath "..\Assets\Intune.ico" + $xamlPath = Join-Path $moduleRoot 'UI\UI.xaml' + $iconPath = Join-Path $moduleRoot 'Assets\Intune.ico' + $iconPngPath = Join-Path $moduleRoot 'Assets\Intune.png' if (-not (Test-Path $xamlPath)) { Write-Error "XAML file not found: $xamlPath" @@ -163,7 +26,7 @@ function Show-IntuneWinAppUtilGui { $ToolVersion = $window.FindName("ToolVersion") $ToolVersionText = $window.FindName("ToolVersionText") $ToolVersionLink = $window.FindName("ToolVersionLink") - $RedownloadTool = $window.FindName("RedownloadTool") + $DownloadTool = $window.FindName("DownloadTool") $FinalFilename = $window.FindName("FinalFilename") @@ -180,7 +43,7 @@ function Show-IntuneWinAppUtilGui { $SourceFolder.Add_TextChanged({ param($sender, $e) $src = $SourceFolder.Text.Trim() - if ($src) { Set-SetupFromSource -SourcePath $src } + if ($src) { Set-SetupFromSource -SourcePath $src -SetupFileControl $SetupFile -FinalFilenameControl $FinalFilename } }) # Preload config.json if it exists @@ -189,7 +52,7 @@ function Show-IntuneWinAppUtilGui { $cfg = Get-Content $configPath -Raw | ConvertFrom-Json if ($cfg.ToolPath -and (Test-Path $cfg.ToolPath)) { $ToolPathBox.Text = $cfg.ToolPath - Show-ToolVersion -Path $cfg.ToolPath + Show-ToolVersion -Path $cfg.ToolPath -Target $ToolVersionText } } catch {} } @@ -200,7 +63,7 @@ function Show-IntuneWinAppUtilGui { if ($dialog.ShowDialog() -eq 'OK') { $SourceFolder.Text = $dialog.SelectedPath # Auto-suggest Invoke-AppDeployToolkit.exe when present in the selected source - Set-SetupFromSource -SourcePath $dialog.SelectedPath + Set-SetupFromSource -SourcePath $dialog.SelectedPath -SetupFileControl $SetupFile -FinalFilenameControl $FinalFilename } }) @@ -214,21 +77,21 @@ function Show-IntuneWinAppUtilGui { if (-not [string]::IsNullOrWhiteSpace($sourceRoot) -and (Test-Path $sourceRoot)) { try { - $relativePath = [System.IO.Path]::GetRelativePath($sourceRoot, $selectedPath) + $relativePath = Get-RelativePath -BasePath $sourceRoot -TargetPath $selectedPath if (-not ($relativePath.StartsWith(".."))) { - # File is inside source folder or subdir - $SetupFile.Text = $relativePath + $SetupFile.Text = $relativePath # File is inside source folder or subdir } else { - # Outside of source folder - $SetupFile.Text = $selectedPath + $SetupFile.Text = $selectedPath # Outside of source folder } } catch { - # If relative path fails (e.g. bad format), fallback - $SetupFile.Text = $selectedPath + $SetupFile.Text = $selectedPath # If relative path fails (e.g. bad format), fallback } + # } else { + # $SetupFile.Text = $selectedPath # Source folder not set or invalid, fallback + # } } else { - # Source folder not set or invalid, fallback - $SetupFile.Text = $selectedPath + $SourceFolder.Text = Split-Path $selectedPath -Parent # Source folder not set or invalid -> infer it from the selected setup path + $SetupFile.Text = [System.IO.Path]::GetFileName($selectedPath) # Store only the file name in SetupFile so it is relative to SourceFolder } } }) @@ -245,24 +108,24 @@ function Show-IntuneWinAppUtilGui { $dlg.Filter = "IntuneWinAppUtil.exe|IntuneWinAppUtil.exe" if ($dlg.ShowDialog() -eq 'OK') { $ToolPathBox.Text = $dlg.FileName - Show-ToolVersion -Path $dlg.FileName + Show-ToolVersion -Path $dlg.FileName -Target $ToolVersionText } }) # Force download the IntuneWinAppUtil.exe tool - $RedownloadTool.Add_Click({ + $DownloadTool.Add_Click({ $confirm = [System.Windows.MessageBox]::Show( - "This will re-download the latest IntuneWinAppUtil.exe and replace the one in your bin folder.`n`nProceed?", - "Confirm re-download", + "This will download the latest IntuneWinAppUtil.exe and replace (if already exists) the one in your bin folder.`n`nProceed?", + "Confirm force download", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Question ) if ($confirm -ne [System.Windows.MessageBoxResult]::Yes) { return } try { - $newPath = Invoke-RedownloadIntuneTool + $newPath = Invoke-DownloadIntuneTool $ToolPathBox.Text = $newPath - Show-ToolVersion -Path $newPath + Show-ToolVersion -Path $newPath -Target $ToolVersionText [System.Windows.MessageBox]::Show( "IntuneWinAppUtil.exe has been refreshed.`n`nPath:`n$newPath", @@ -272,7 +135,7 @@ function Show-IntuneWinAppUtilGui { ) } catch { [System.Windows.MessageBox]::Show( - "Re-download failed:`n$($_.Exception.Message)", + "Download failed:`n$($_.Exception.Message)", "Error", [System.Windows.MessageBoxButton]::OK, [System.Windows.MessageBoxImage]::Error @@ -284,122 +147,189 @@ function Show-IntuneWinAppUtilGui { $ToolPathBox.Add_TextChanged({ param($sender, $e) $p = $ToolPathBox.Text.Trim() - if ($p) { Show-ToolVersion -Path $p } else { Show-ToolVersion -Path $null } + if ($p) { Show-ToolVersion -Path $p -Target $ToolVersionText } else { Show-ToolVersion -Path $null -Target $ToolVersionText } + }) + + # If the user typed/pasted an absolute setup path before setting SourceFolder, + # infer SourceFolder from that path and convert SetupFile to a relative file name. + $SetupFile.Add_LostFocus({ + $sText = $SetupFile.Text.Trim() + # Only act if SourceFolder is empty and SetupFile looks like an absolute existing path + if ([string]::IsNullOrWhiteSpace($SourceFolder.Text) -and + -not [string]::IsNullOrWhiteSpace($sText) -and + [System.IO.Path]::IsPathRooted($sText) -and + (Test-Path $sText)) { + + $SourceFolder.Text = Split-Path $sText -Parent + $SetupFile.Text = [System.IO.Path]::GetFileName($sText) + # Note: SourceFolder.Text change will NOT override SetupFile because Set-SetupFromSource + # early-returns if SetupFile already points to an existing file (absolute or relative). + } }) + # Run button: validate inputs, run IntuneWinAppUtil.exe, rename output if needed $RunButton.Add_Click({ - $c = $SourceFolder.Text.Trim() - $s = $SetupFile.Text.Trim() - $o = $OutputFolder.Text.Trim() - $f = $FinalFilename.Text.Trim() + $c = $SourceFolder.Text.Trim() # Source folder + $s = $SetupFile.Text.Trim() # Setup file (relative or absolute) + $o = $OutputFolder.Text.Trim() # Output folder + $f = $FinalFilename.Text.Trim() # Final filename + # Clean FinalFilename from invalid chars $f = -join ($f.ToCharArray() | Where-Object { [System.IO.Path]::GetInvalidFileNameChars() -notcontains $_ }) + # Validate source folder if (-not (Test-Path $c)) { [System.Windows.MessageBox]::Show("Invalid source folder path.", "Error", "OK", "Error"); return } + + # Validate setup file if (-not (Test-Path $s)) { $s = Join-Path $c $s if (-not (Test-Path $s)) { [System.Windows.MessageBox]::Show("Setup file not found.", "Error", "OK", "Error"); return } } - if (-not (Test-Path $o)) { [System.Windows.MessageBox]::Show("Invalid output folder path.", "Error", "OK", "Error"); return } - - # IntuneWinAppUtil.exe path check (or download if not set) - $toolPath = $ToolPathBox.Text.Trim() - $downloadDir = Join-Path $env:APPDATA "IntuneWinAppUtilGUI\bin" - $exePath = Join-Path $downloadDir "IntuneWinAppUtil.exe" - if ([string]::IsNullOrWhiteSpace($toolPath) -or -not (Test-Path $toolPath)) { - if (-not (Test-Path $exePath)) { - try { - $url = "https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/archive/refs/heads/master.zip" - $zipPath = Join-Path $env:TEMP "IntuneWinAppUtil-master.zip" - $extractPath = Join-Path $env:TEMP "IntuneExtract" - - if (Test-Path $extractPath) { Remove-Item $extractPath -Recurse -Force } + # Validate extension before running the tool + $extSetup = [System.IO.Path]::GetExtension($s).ToLowerInvariant() + if ($extSetup -notin @(".exe", ".msi")) { + [System.Windows.MessageBox]::Show( + "Setup file must be .exe or .msi (got '$extSetup').", + "Invalid setup type", "OK", "Error" + ) + return + } - Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing - [System.IO.Compression.ZipFile]::ExtractToDirectory($zipPath, $extractPath, $true) - Remove-Item $zipPath -Force + # Validate output folder + if (-not (Test-Path $o)) { + try { + New-Item -Path $o -ItemType Directory -Force | Out-Null + } catch { + [System.Windows.MessageBox]::Show("Output folder path is invalid and could not be created.", "Error", "OK", "Error") + return + } + } - # Find the executable in the extracted structure - $sourceExe = Get-ChildItem -Path $extractPath -Recurse -Filter "IntuneWinAppUtil.exe" | Select-Object -First 1 + # Normalize all paths to absolute + try { + $c = [System.IO.Path]::GetFullPath($c) + $s = [System.IO.Path]::GetFullPath($s) + $o = [System.IO.Path]::GetFullPath($o) + } catch { + [System.Windows.MessageBox]::Show("Invalid path format: $($_.Exception.Message)", "Error", "OK", "Error") + return + } - if (-not $sourceExe) { - throw "IntuneWinAppUtil.exe not found in extracted archive." - } + # IntuneWinAppUtil.exe path check (or initialize/download if not set) + $toolPath = Initialize-IntuneWinAppUtil -UiToolPath ($ToolPathBox.Text.Trim()) - # Copy to destination - New-Item -ItemType Directory -Force -Path $downloadDir | Out-Null - Copy-Item -Path $sourceExe.FullName -Destination $exePath -Force + if (-not $toolPath -or -not (Test-Path $toolPath)) { + [System.Windows.MessageBox]::Show( + "IntuneWinAppUtil.exe not found and could not be initialized.", + "Error", "OK", "Error" + ) + return + } - [System.Windows.MessageBox]::Show("Tool downloaded and extracted to:`n$exePath", "Download Complete", "OK", "Info") - } catch { - [System.Windows.MessageBox]::Show("Failed to download or extract the archive:`n$($_.Exception.Message)", "Download Error", "OK", "Error") - return - } - } + # Keep UI in sync and show version + $ToolPathBox.Text = $toolPath + Show-ToolVersion -Path $toolPath -Target $ToolVersionText + + # Build a single, properly-quoted argument string + # -c = source folder, -s = setup file (EXE/MSI), -o = output folder. + $iwaArgs = ('-c "{0}" -s "{1}" -o "{2}"' -f $c, $s, $o) - if (Test-Path $exePath) { - $toolPath = $exePath - $ToolPathBox.Text = $toolPath - Show-ToolVersion -Path $toolPath # equivalent -Path $exePath - } + # Launch IntuneWinAppUtil.exe, wait, and capture exit code (WorkingDirectory is set to the tool's folder to avoid relative path issues) + try { + $proc = Start-Process -FilePath $toolPath ` + -ArgumentList $iwaArgs ` + -WorkingDirectory (Split-Path $toolPath) ` + -WindowStyle Normal ` + -PassThru + } catch { + [System.Windows.MessageBox]::Show( + "Failed to start IntuneWinAppUtil.exe:`n$($_.Exception.Message)", + "Execution error", "OK", "Error" + ) + return } + $proc.WaitForExit() - if (-not (Test-Path $toolPath)) { - [System.Windows.MessageBox]::Show("IntuneWinAppUtil.exe not found at:`n$toolPath", "Error", "OK", "Error") + # Fail early if tool returned non-zero + if ($proc.ExitCode -ne 0) { + [System.Windows.MessageBox]::Show( + "IntuneWinAppUtil exited with code $($proc.ExitCode).", + "Packaging failed", "OK", "Error" + ) return } - $IWAUtilargs = "-c `"$c`" -s `"$s`" -o `"$o`"" - Start-Process -FilePath $toolPath -ArgumentList $IWAUtilargs -WorkingDirectory (Split-Path $toolPath) -WindowStyle Normal -Wait - - Start-Sleep -Seconds 1 + # Wait a bit for the output file to appear (up to 10 seconds, checking every 250ms) + # Compute the default output filename that IntuneWinAppUtil generates. By default it matches the setup's base name + ".intunewin". $defaultName = [System.IO.Path]::GetFileNameWithoutExtension($s) + ".intunewin" $defaultPath = Join-Path $o $defaultName + $timeoutSec = 10 + $elapsed = 0 + while (-not (Test-Path $defaultPath) -and $elapsed -lt $timeoutSec) { + Start-Sleep -Milliseconds 250 + $elapsed += 0.25 + } + if (Test-Path $defaultPath) { - $newName = if ([string]::IsNullOrWhiteSpace($f)) { - (Split-Path $c -Leaf) + ".intunewin" + # Build desired name from $f (if any), ensuring exactly one ".intunewin": + # - If FinalFilename ($f) is blank, fallback to using the source folder name. + # - Otherwise use the provided FinalFilename. + if ([string]::IsNullOrWhiteSpace($f)) { + $desiredName = (Split-Path $c -Leaf) + ".intunewin" } else { - $f + ".intunewin" + $extF = [System.IO.Path]::GetExtension($f).ToLowerInvariant() + $baseF = if ($extF -eq ".intunewin") { [System.IO.Path]::GetFileNameWithoutExtension($f) } else { $f } + $desiredName = $baseF + ".intunewin" } + $newName = $desiredName + try { + # Prepare collision-safe rename: + # If a file with the desired name already exists, append _1, _2, ... until unique. $baseName = [System.IO.Path]::GetFileNameWithoutExtension($newName) $ext = [System.IO.Path]::GetExtension($newName) $finalName = $newName $counter = 1 - + while (Test-Path (Join-Path $o $finalName)) { $finalName = "$baseName" + "_$counter" + "$ext" $counter++ } - + + # Perform the rename operation from the tool's default output to our final target name. Rename-Item -Path $defaultPath -NewName $finalName -Force $fullPath = Join-Path $o $finalName - + + # Inform the user and optionally offer to open File Explorer with the file selected. $msg = "Package created and renamed to:`n$finalName" if ($finalName -ne $newName) { $msg += "`n(Note: original name '$newName' already existed.)" } $msg += "`n`nOpen folder?" - + $resp = [System.Windows.MessageBox]::Show( $msg, "Success", [System.Windows.MessageBoxButton]::YesNo, [System.Windows.MessageBoxImage]::Information ) + if ($resp -eq "Yes") { - Start-Process explorer.exe "/select,`"$fullPath`"" + Start-Process explorer.exe "/select,`"$fullPath`"" # Open Explorer with the new file pre-selected. } - + } catch { - [System.Windows.MessageBox]::Show("Renaming failed: $($_.Exception.Message)", "Warning", "OK", "Warning") + [System.Windows.MessageBox]::Show("Renaming failed: $($_.Exception.Message)", "Warning", "OK", "Warning") # If anything goes wrong during the rename, show a warning. } - + } else { - [System.Windows.MessageBox]::Show("Output file not found.", "Warning", "OK", "Warning") + [System.Windows.MessageBox]::Show( + "Output file not found:`n$defaultPath", + "Warning", "OK", "Warning" + ) # The expected output was not found; warn the user (the tool may have failed). } }) @@ -416,6 +346,7 @@ function Show-IntuneWinAppUtilGui { $window.Close() }) + # Keyboard shortcuts: Esc to exit (with confirmation), Enter to run packaging $window.Add_KeyDown({ param($sender, $e) switch ($e.Key) { @@ -430,6 +361,7 @@ function Show-IntuneWinAppUtilGui { } }) + # When the window is closed, save the ToolPath to config.json $window.Add_Closed({ if (-not (Test-Path (Split-Path $configPath))) { New-Item -Path (Split-Path $configPath) -ItemType Directory -Force | Out-Null @@ -438,10 +370,25 @@ function Show-IntuneWinAppUtilGui { $cfg | ConvertTo-Json | Set-Content $configPath -Encoding UTF8 }) + # Set window icon if available if (Test-Path $iconPath) { $window.Icon = [System.Windows.Media.Imaging.BitmapFrame]::Create((New-Object System.Uri $iconPath, [System.UriKind]::Absolute)) } + # Find the Image control in XAML and load the PNG from disk and assign it to the Image.Source + $HeaderIcon = $window.FindName('HeaderIcon') + if ($HeaderIcon -and (Test-Path $iconPngPath)) { + # Use BitmapImage with OnLoad so the file is not locked after loading + $bmp = New-Object System.Windows.Media.Imaging.BitmapImage + $bmp.BeginInit() + $bmp.CacheOption = [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad + $bmp.UriSource = [Uri]::new($iconPngPath, [UriKind]::Absolute) + $bmp.EndInit() + + $HeaderIcon.Source = $bmp + } + + # Hyperlink in the ToolVersionText to open the GitHub version history page (and other links if needed) $window.AddHandler([ System.Windows.Documents.Hyperlink]::RequestNavigateEvent, [System.Windows.Navigation.RequestNavigateEventHandler] { diff --git a/README.md b/README.md index 301f6d9..99c99bc 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ This tool simplifies the packaging of Win32 apps for Microsoft Intune by providi ## 🔧 Features - Built with **WPF** (XAML) and **PowerShell** — no external dependencies. -- Automatically stores tool path and reuses it on next launch (saved in a JSON file, check [section "Configuration file"](#%EF%B8%8F-configuration-file)). +- Automatically stores tool path and reuses it on next launch (saved in a JSON file, check ["Configuration file"](#%EF%B8%8F-configuration-file)). - Graphical interface for all required options (`-c`, `-s`, `-o`). - **Auto-download** of the latest version of `IntuneWinAppUtil.exe` from GitHub (optional). - It detects the use of PSAppDeployToolkit and automatically proposes executable file and final IntuneWin package name. @@ -26,7 +26,7 @@ This tool simplifies the packaging of Win32 apps for Microsoft Intune by providi - Windows 10 or later. - PowerShell 5.1 or higher. -- .NET Framework (usually already installed on supported systems). +- .NET Framework 4.7.2 or higher (usually already installed on supported systems). --- diff --git a/UI/UI.xaml b/UI/UI.xaml index dbe4868..cd0fcdb 100644 --- a/UI/UI.xaml +++ b/UI/UI.xaml @@ -7,140 +7,159 @@ MinHeight="360" WindowStartupLocation="CenterScreen"> - - - - - - - - - - - - - - - - - - -