Skip to content

Commit a3c8a0e

Browse files
authored
Merge pull request #4 from gioxx/1.0.4
1.0.4
2 parents 488a817 + 6a7abb5 commit a3c8a0e

12 files changed

+663
-375
lines changed

Assets/Intune.png

28.8 KB
Loading

IntuneWinAppUtilGUI.psd1

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,34 @@
11
@{
2-
RootModule = 'IntuneWinAppUtilGUI.psm1'
3-
ModuleVersion = '1.0.3'
4-
GUID = '7db79126-1b57-48d2-970a-4795692dfcfc'
5-
Author = 'Giovanni Solone'
6-
Description = 'GUI wrapper for IntuneWinAppUtil.exe with config file support and WPF interface.'
2+
RootModule = 'IntuneWinAppUtilGUI.psm1'
3+
ModuleVersion = '1.0.4'
4+
GUID = '7db79126-1b57-48d2-970a-4795692dfcfc'
5+
Author = 'Giovanni Solone'
6+
Description = 'GUI wrapper for IntuneWinAppUtil.exe with config file support and WPF interface.'
77

8-
PowerShellVersion = '5.1'
8+
# Minimum required PowerShell (PS 5.1 works; better with PS 7+)
9+
PowerShellVersion = '5.1'
10+
CompatiblePSEditions = @('Desktop', 'Core')
11+
RequiredAssemblies = @()
12+
FunctionsToExport = @()
13+
CmdletsToExport = @()
14+
VariablesToExport = @()
15+
AliasesToExport = @()
916

10-
RequiredAssemblies = @(
11-
'System.Windows.Forms',
12-
'PresentationFramework'
13-
)
14-
15-
FunctionsToExport = @('Show-IntuneWinAppUtilGui')
16-
17-
CmdletsToExport = @()
18-
VariablesToExport = @()
19-
AliasesToExport = @()
20-
21-
PrivateData = @{
17+
PrivateData = @{
2218
PSData = @{
23-
Tags = @('Intune', 'Win32', 'GUI', 'packaging', 'IntuneWinAppUtil', 'IntuneWinAppUtil.exe', 'Microsoft', 'PowerShell', 'PSADT', 'AppDeployToolkit')
24-
License = 'MIT'
19+
Tags = @('Intune', 'Win32', 'GUI', 'packaging', 'IntuneWinAppUtil', 'Microsoft', 'PowerShell', 'PSADT', 'AppDeployToolkit')
2520
ProjectUri = 'https://github.com/gioxx/IntuneWinAppUtilGUI'
26-
Icon = 'icon.png'
27-
Readme = 'README.md'
21+
LicenseUri = 'https://opensource.org/licenses/MIT'
22+
IconUri = 'https://raw.githubusercontent.com/gioxx/IntuneWinAppUtilGUI/main/Assets/icon.png'
2823
ReleaseNotes = @'
29-
- 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.
30-
- 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.
24+
- Bugfix: Removed any reference to ZIP uploads as setup files.
25+
- Bugfix: Fixed PS 5.1 incompatibility in relative-path resolution ([System.IO.Path]::GetRelativePath is PS 7+).
26+
- Improved: Code cleanup, removed redundant GitHub download logic; refactoring.
27+
- Improved: Validates setup file existence and type.
28+
- Improved: Tries to create output folder when missing.
29+
- Improved: Ensures exactly one ".intunewin" extension on output.
30+
- Improved: If Source folder is not specified, it is inferred from Setup file.
31+
- Improved: Added more inline comments for maintainability.
3132
'@
3233
}
3334
}

IntuneWinAppUtilGUI.psm1

Lines changed: 36 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,37 @@
1-
# # Import helper functions
2-
# Get-ChildItem -Path "$PSScriptRoot\Private" -Filter *.ps1 | ForEach-Object {
3-
# . $_.FullName
4-
# }
5-
6-
# Import public functions
7-
Get-ChildItem -Path "$PSScriptRoot\Public" -Filter *.ps1 | ForEach-Object {
8-
. $_.FullName
1+
# IntuneWinAppUtilGUI.psm1
2+
3+
# Load UI-related assemblies once at module import time
4+
Add-Type -AssemblyName PresentationFramework
5+
Add-Type -AssemblyName System.IO.Compression.FileSystem
6+
Add-Type -AssemblyName System.Windows.Forms
7+
8+
# Expose module root so public/private scripts can resolve resources (UI.xaml, Assets, etc.)
9+
$script:ModuleRoot = $PSScriptRoot
10+
11+
# --- Load Private helpers first (NOT exported) ---
12+
$privateDir = Join-Path $PSScriptRoot 'Private'
13+
if (Test-Path $privateDir) {
14+
Get-ChildItem -Path $privateDir -Filter '*.ps1' -File | ForEach-Object {
15+
try {
16+
. $_.FullName # dot-source
17+
} catch {
18+
throw "Failed to load Private script '$($_.Name)': $($_.Exception.Message)"
19+
}
20+
}
921
}
22+
23+
# --- Load Public entry points (will be exported) ---
24+
$publicDir = Join-Path $PSScriptRoot 'Public'
25+
if (Test-Path $publicDir) {
26+
Get-ChildItem -Path $publicDir -Filter '*.ps1' -File | ForEach-Object {
27+
try {
28+
. $_.FullName # dot-source
29+
} catch {
30+
throw "Failed to load Public script '$($_.Name)': $($_.Exception.Message)"
31+
}
32+
}
33+
}
34+
35+
# Export only the intended public functions
36+
# (Add more names here as you create additional public commands)
37+
Export-ModuleMember -Function 'Show-IntuneWinAppUtilGui'

Private/Get-ExeVersion.ps1

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
function Get-ExeVersion {
2+
<#
3+
.SYNOPSIS
4+
Returns file version (FileVersion preferred, then ProductVersion); $null if not available.
5+
.DESCRIPTION
6+
- Uses [System.Diagnostics.FileVersionInfo] to read version metadata.
7+
- Prefers FileVersion, falls back to ProductVersion.
8+
.PARAMETER Path
9+
Absolute path to the executable file.
10+
.OUTPUTS
11+
[string] or $null
12+
#>
13+
14+
[CmdletBinding()]
15+
param(
16+
[Parameter(Mandatory)][string]$Path
17+
)
18+
19+
try {
20+
if (-not (Test-Path $Path)) { return $null }
21+
$vi = [System.Diagnostics.FileVersionInfo]::GetVersionInfo($Path)
22+
if ($vi.FileVersion -and $vi.FileVersion.Trim()) { return $vi.FileVersion.Trim() }
23+
if ($vi.ProductVersion -and $vi.ProductVersion.Trim()) { return $vi.ProductVersion.Trim() }
24+
return $null
25+
} catch {
26+
return $null
27+
}
28+
}

Private/Get-RelativePath.ps1

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
function Get-RelativePath {
2+
<#
3+
.SYNOPSIS
4+
Returns a relative Windows path from BasePath to TargetPath when possible;
5+
otherwise returns the absolute, normalized TargetPath.
6+
.DESCRIPTION
7+
- Normalizes base and target paths via [System.IO.Path]::GetFullPath.
8+
- If paths are on different roots (drive letters or UNC shares), falls back to absolute.
9+
- Uses Uri.MakeRelativeUri to compute the relative portion.
10+
- Decodes URL-encoded characters and converts forward slashes to backslashes.
11+
.PARAMETER BasePath
12+
The base directory you want to compute the relative path from.
13+
.PARAMETER TargetPath
14+
The file or directory path you want to compute the relative path to.
15+
.OUTPUTS
16+
[string] Relative path if computable, otherwise absolute normalized TargetPath.
17+
#>
18+
19+
[CmdletBinding()]
20+
param(
21+
[Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$BasePath,
22+
[Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$TargetPath
23+
)
24+
25+
try {
26+
# Normalize and ensure BasePath ends with a directory separator so Uri treats it as a folder
27+
$baseFull = [System.IO.Path]::GetFullPath(($BasePath.TrimEnd('\') + '\'))
28+
$targetFull = [System.IO.Path]::GetFullPath($TargetPath)
29+
30+
# If roots differ (e.g., C:\ vs D:\ or different UNC shares), relative path is not possible
31+
$baseRoot = [System.IO.Path]::GetPathRoot($baseFull)
32+
$targetRoot = [System.IO.Path]::GetPathRoot($targetFull)
33+
if (-not [string]::Equals($baseRoot, $targetRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
34+
return $targetFull
35+
}
36+
37+
# Compute the relative URI and convert it to a Windows path
38+
$uriBase = [Uri]$baseFull
39+
$uriTarget = [Uri]$targetFull
40+
41+
$rel = $uriBase.MakeRelativeUri($uriTarget).ToString()
42+
# Decode URL-encoded chars (e.g., spaces) and switch to backslashes
43+
$relWin = [Uri]::UnescapeDataString($rel).Replace('/', '\')
44+
45+
return $relWin
46+
} catch {
47+
# On any unexpected error, just return the original target (best-effort behavior)
48+
return $TargetPath
49+
}
50+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
function Initialize-IntuneWinAppUtil {
2+
<#
3+
.SYNOPSIS
4+
Returns a valid IntuneWinAppUtil.exe path or $null on failure.
5+
.DESCRIPTION
6+
- If a UI-provided path is valid, use it.
7+
- Else, use cached copy under %APPDATA%\IntuneWinAppUtilGUI\bin.
8+
- Else, download the latest via Invoke-DownloadIntuneTool (private helper).
9+
.PARAMETER UiToolPath
10+
Optional path provided by the UI (textbox).
11+
.OUTPUTS
12+
[string] or $null
13+
#>
14+
15+
[CmdletBinding()]
16+
param(
17+
[Parameter(Mandatory = $false)][string]$UiToolPath
18+
)
19+
20+
try {
21+
$appRoot = Join-Path $env:APPDATA 'IntuneWinAppUtilGUI'
22+
$binDir = Join-Path $appRoot 'bin'
23+
$exePath = Join-Path $binDir 'IntuneWinAppUtil.exe'
24+
25+
if (-not [string]::IsNullOrWhiteSpace($UiToolPath) -and (Test-Path $UiToolPath)) { return $UiToolPath }
26+
if (Test-Path $exePath) { return $exePath }
27+
28+
# Fallback: download latest tool
29+
return (Invoke-DownloadIntuneTool)
30+
} catch {
31+
return $null
32+
}
33+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
function Invoke-DownloadIntuneTool {
2+
<#
3+
.SYNOPSIS
4+
Downloads the latest IntuneWinAppUtil.exe from GitHub and caches it under
5+
%APPDATA%\IntuneWinAppUtilGUI\bin.
6+
.DESCRIPTION
7+
- Forces TLS 1.2 for GitHub downloads.
8+
- Creates (or ensures) the bin directory under $env:APPDATA\IntuneWinAppUtilGUI\bin.
9+
- Removes any stale IntuneWinAppUtil.exe in the bin directory.
10+
- Downloads the repository master ZIP, extracts it to a unique temp folder,
11+
locates IntuneWinAppUtil.exe, and copies it into the bin directory.
12+
- Cleans up all temp files/folders in a finally block.
13+
- Returns the full path to the cached IntuneWinAppUtil.exe.
14+
- Throws on failure (caller can catch and show a message box).
15+
.PARAMETER DestinationRoot
16+
Optional base path for the cache (default: $env:APPDATA\IntuneWinAppUtilGUI).
17+
.PARAMETER RepoZipUrl
18+
Optional ZIP URL (default: master branch of Microsoft-Win32-Content-Prep-Tool).
19+
.OUTPUTS
20+
[string] Full path to IntuneWinAppUtil.exe.
21+
.EXAMPLE
22+
$exe = Invoke-DownloadIntuneTool
23+
# $exe now points to %APPDATA%\IntuneWinAppUtilGUI\bin\IntuneWinAppUtil.exe
24+
#>
25+
26+
[CmdletBinding()]
27+
param(
28+
[string]$DestinationRoot = (Join-Path $env:APPDATA 'IntuneWinAppUtilGUI'),
29+
[string]$RepoZipUrl = 'https://github.com/microsoft/Microsoft-Win32-Content-Prep-Tool/archive/refs/heads/master.zip'
30+
)
31+
32+
# Ensure TLS 1.2 for GitHub
33+
try { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 } catch {}
34+
35+
$binDir = Join-Path $DestinationRoot 'bin'
36+
$exePath = Join-Path $binDir 'IntuneWinAppUtil.exe'
37+
$tempZip = Join-Path $env:TEMP ("IntuneWinAppUtil-{0}.zip" -f ([guid]::NewGuid()))
38+
$tempDir = Join-Path $env:TEMP ("IntuneExtract-{0}" -f ([guid]::NewGuid()))
39+
40+
try {
41+
# Prepare target folder and clean stale exe
42+
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
43+
if (Test-Path $exePath) { Remove-Item $exePath -Force -ErrorAction SilentlyContinue }
44+
45+
# Download
46+
Invoke-WebRequest -Uri $RepoZipUrl -OutFile $tempZip -UseBasicParsing -ErrorAction Stop
47+
48+
# Extract (fallback if overwrite overload isn't available)
49+
New-Item -ItemType Directory -Path $tempDir -Force | Out-Null
50+
$zipType = [System.IO.Compression.ZipFile]
51+
$hasOverwrite = $zipType.GetMethod(
52+
'ExtractToDirectory',
53+
[Reflection.BindingFlags]'Public, Static',
54+
$null,
55+
@([string], [string], [bool]),
56+
$null
57+
)
58+
if ($hasOverwrite) {
59+
[System.IO.Compression.ZipFile]::ExtractToDirectory($tempZip, $tempDir, $true)
60+
} else {
61+
[System.IO.Compression.ZipFile]::ExtractToDirectory($tempZip, $tempDir)
62+
}
63+
64+
# Locate exe
65+
$found = Get-ChildItem -Path $tempDir -Recurse -Filter 'IntuneWinAppUtil.exe' -File -ErrorAction SilentlyContinue | Select-Object -First 1
66+
if (-not $found) {
67+
throw "IntuneWinAppUtil.exe not found in the extracted archive."
68+
}
69+
70+
# Copy into cache
71+
Copy-Item -Path $found.FullName -Destination $exePath -Force
72+
return $exePath
73+
} catch {
74+
throw $_
75+
} finally {
76+
# Best-effort cleanup
77+
foreach ($p in @($tempZip, $tempDir)) {
78+
try {
79+
if (Test-Path $p) {
80+
if (Test-Path $p -PathType Container) {
81+
Remove-Item $p -Recurse -Force -ErrorAction SilentlyContinue
82+
} else {
83+
Remove-Item $p -Force -ErrorAction SilentlyContinue
84+
}
85+
}
86+
} catch {}
87+
}
88+
}
89+
}

Private/Set-SetupFromSource.ps1

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
function Set-SetupFromSource {
2+
<#
3+
.SYNOPSIS
4+
Suggests the setup file and (optionally) proposes the final package name from a given source folder.
5+
.DESCRIPTION
6+
- Recursively searches for 'Invoke-AppDeployToolkit.exe' under SourcePath.
7+
- If found, populates the provided TextBox control (SetupFileControl) with a relative path
8+
(via Get-RelativePath) when the exe resides under SourcePath.
9+
- Does not overwrite SetupFileControl if it already points to an existing file (absolute
10+
or relative to SourcePath).
11+
- If 'Invoke-AppDeployToolkit.ps1' exists in the same folder, extracts AppName/AppVersion
12+
and sets FinalFilenameControl.Text to 'AppName_Version' (sanitizing spaces and invalid
13+
filename characters).
14+
- Fails silently on parsing/IO errors.
15+
.PARAMETER SourcePath
16+
The source directory to inspect. Must exist.
17+
.PARAMETER SetupFileControl
18+
The TextBox to populate with the suggested setup path (relative when possible).
19+
.PARAMETER FinalFilenameControl
20+
The TextBox to populate with the proposed final filename (e.g., 'AppName_Version').
21+
.OUTPUTS
22+
None. Mutates the provided TextBox controls.
23+
.EXAMPLE
24+
Set-SetupFromSource -SourcePath $SourceFolder.Text -SetupFileControl $SetupFile -FinalFilenameControl $FinalFilename
25+
#>
26+
27+
[CmdletBinding()]
28+
param(
29+
[Parameter(Mandatory)][ValidateNotNullOrEmpty()][string]$SourcePath,
30+
[Parameter(Mandatory)][ValidateNotNull()][System.Windows.Controls.TextBox]$SetupFileControl,
31+
[Parameter(Mandatory)][ValidateNotNull()][System.Windows.Controls.TextBox]$FinalFilenameControl
32+
)
33+
34+
if (-not (Test-Path $SourcePath)) { return }
35+
36+
# If current SetupFile value already points to an existing file (absolute or relative to source), do not override.
37+
$current = $SetupFileControl.Text.Trim()
38+
if ($current) {
39+
if (Test-Path $current) { return }
40+
$maybeRelative = Join-Path $SourcePath $current
41+
if (Test-Path $maybeRelative) { return }
42+
}
43+
44+
# Search for Invoke-AppDeployToolkit.exe
45+
$exeHit = Get-ChildItem -Path $SourcePath -Filter 'Invoke-AppDeployToolkit.exe' -File -Recurse -ErrorAction SilentlyContinue | Select-Object -First 1
46+
if ($exeHit) {
47+
# Prefer a relative path when the file is inside the source folder
48+
$SetupFileControl.Text = Get-RelativePath -BasePath $SourcePath -TargetPath $exeHit.FullName
49+
50+
# Look for Invoke-AppDeployToolkit.ps1 in the same folder
51+
$ps1Path = Join-Path $exeHit.Directory.FullName 'Invoke-AppDeployToolkit.ps1'
52+
if (Test-Path $ps1Path) {
53+
try {
54+
$content = Get-Content $ps1Path -Raw
55+
$appName = $null
56+
$appVersion = $null
57+
if ($content -match "AppName\s*=\s*'([^']+)'") { $appName = $matches[1] }
58+
if ($content -match "AppVersion\s*=\s*'([^']+)'") { $appVersion = $matches[1] }
59+
60+
if ($appName -and $appVersion) {
61+
# Clean filename: remove spaces and invalid chars
62+
$cleanName = ($appName -replace '\s+', '' -replace '[\\/:*?"<>|]', '-')
63+
$cleanVer = ($appVersion -replace '\s+', '' -replace '[\\/:*?"<>|]', '-')
64+
$FinalFilenameControl.Text = "${cleanName}_${cleanVer}"
65+
}
66+
} catch {
67+
# fail silently
68+
}
69+
}
70+
}
71+
}

0 commit comments

Comments
 (0)