Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added Assets/Intune.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 25 additions & 24 deletions IntuneWinAppUtilGUI.psd1
Original file line number Diff line number Diff line change
@@ -1,33 +1,34 @@
@{
RootModule = 'IntuneWinAppUtilGUI.psm1'
ModuleVersion = '1.0.3'
GUID = '7db79126-1b57-48d2-970a-4795692dfcfc'
Author = 'Giovanni Solone'
Description = 'GUI wrapper for IntuneWinAppUtil.exe with config file support and WPF interface.'
RootModule = 'IntuneWinAppUtilGUI.psm1'
ModuleVersion = '1.0.4'
GUID = '7db79126-1b57-48d2-970a-4795692dfcfc'
Author = 'Giovanni Solone'
Description = 'GUI wrapper for IntuneWinAppUtil.exe with config file support and WPF interface.'

PowerShellVersion = '5.1'
# Minimum required PowerShell (PS 5.1 works; better with PS 7+)
PowerShellVersion = '5.1'
CompatiblePSEditions = @('Desktop', 'Core')
RequiredAssemblies = @()
FunctionsToExport = @()
CmdletsToExport = @()
VariablesToExport = @()
AliasesToExport = @()

RequiredAssemblies = @(
'System.Windows.Forms',
'PresentationFramework'
)

FunctionsToExport = @('Show-IntuneWinAppUtilGui')

CmdletsToExport = @()
VariablesToExport = @()
AliasesToExport = @()

PrivateData = @{
PrivateData = @{
PSData = @{
Tags = @('Intune', 'Win32', 'GUI', 'packaging', 'IntuneWinAppUtil', 'IntuneWinAppUtil.exe', 'Microsoft', 'PowerShell', 'PSADT', 'AppDeployToolkit')
License = 'MIT'
Tags = @('Intune', 'Win32', 'GUI', 'packaging', 'IntuneWinAppUtil', 'Microsoft', 'PowerShell', 'PSADT', 'AppDeployToolkit')
ProjectUri = 'https://github.com/gioxx/IntuneWinAppUtilGUI'
Icon = 'icon.png'
Readme = 'README.md'
LicenseUri = 'https://opensource.org/licenses/MIT'
IconUri = 'https://raw.githubusercontent.com/gioxx/IntuneWinAppUtilGUI/main/Assets/icon.png'
ReleaseNotes = @'
- NEW: PSADT - If Invoke-AppDeployToolkit.exe is detected in the source folder, it is proposed as the default setup file. If Invoke-AppDeployToolkit.ps1 is detected in the source folder, it is parsed to propose a name for the IntuneWin package.
- Improved: The version of the IntuneWinAppUtil.exe file in use is shown on the screen. You can also use the "Force download" button to download the latest version available from GitHub. The list of versions is available at the "Version history" link.
- 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.
'@
}
}
Expand Down
44 changes: 36 additions & 8 deletions IntuneWinAppUtilGUI.psm1
Original file line number Diff line number Diff line change
@@ -1,9 +1,37 @@
# # Import helper functions
# Get-ChildItem -Path "$PSScriptRoot\Private" -Filter *.ps1 | ForEach-Object {
# . $_.FullName
# }

# Import public functions
Get-ChildItem -Path "$PSScriptRoot\Public" -Filter *.ps1 | ForEach-Object {
. $_.FullName
# IntuneWinAppUtilGUI.psm1

# Load UI-related assemblies once at module import time
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName System.IO.Compression.FileSystem
Add-Type -AssemblyName System.Windows.Forms

# Expose module root so public/private scripts can resolve resources (UI.xaml, Assets, etc.)
$script:ModuleRoot = $PSScriptRoot

# --- Load Private helpers first (NOT exported) ---
$privateDir = Join-Path $PSScriptRoot 'Private'
if (Test-Path $privateDir) {
Get-ChildItem -Path $privateDir -Filter '*.ps1' -File | ForEach-Object {
try {
. $_.FullName # dot-source
} catch {
throw "Failed to load Private script '$($_.Name)': $($_.Exception.Message)"
}
}
}

# --- Load Public entry points (will be exported) ---
$publicDir = Join-Path $PSScriptRoot 'Public'
if (Test-Path $publicDir) {
Get-ChildItem -Path $publicDir -Filter '*.ps1' -File | ForEach-Object {
try {
. $_.FullName # dot-source
} catch {
throw "Failed to load Public script '$($_.Name)': $($_.Exception.Message)"
}
}
}

# Export only the intended public functions
# (Add more names here as you create additional public commands)
Export-ModuleMember -Function 'Show-IntuneWinAppUtilGui'
28 changes: 28 additions & 0 deletions Private/Get-ExeVersion.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
function Get-ExeVersion {
<#
.SYNOPSIS
Returns file version (FileVersion preferred, then ProductVersion); $null if not available.
.DESCRIPTION
- Uses [System.Diagnostics.FileVersionInfo] to read version metadata.
- Prefers FileVersion, falls back to ProductVersion.
.PARAMETER Path
Absolute path to the executable file.
.OUTPUTS
[string] or $null
#>

[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
}
}
50 changes: 50 additions & 0 deletions Private/Get-RelativePath.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}
33 changes: 33 additions & 0 deletions Private/Initialize-IntuneWinAppUtil.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}
89 changes: 89 additions & 0 deletions Private/Invoke-DownloadIntuneTool.ps1
Original file line number Diff line number Diff line change
@@ -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 {}
}
}
}
71 changes: 71 additions & 0 deletions Private/Set-SetupFromSource.ps1
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
Loading