feat(extension/podman): switch to podman msi installer (#15441)

* feat(extension/podman): switch to podman msi installer

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

* revert: unnecessary change to package.json

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>

---------

Signed-off-by: axel7083 <42176370+axel7083@users.noreply.github.com>
This commit is contained in:
axel7083 2026-01-16 13:59:49 +01:00 committed by GitHub
parent 640cb36a64
commit 76ec5159cd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 89 additions and 44 deletions

View file

@ -157,11 +157,11 @@ const config = {
});
// add podman installer
if (context.arch === Arch.x64) {
context.packager.config.extraResources.push(`${PODMAN_EXTENSION_ASSETS}/podman-installer-windows-amd64.exe`);
context.packager.config.extraResources.push(`${PODMAN_EXTENSION_ASSETS}/podman-installer-windows-amd64.msi`);
context.packager.config.extraResources.push(`${PODMAN_EXTENSION_ASSETS}/podman-image-x64.zst`);
}
if (context.arch === Arch.arm64) {
context.packager.config.extraResources.push(`${PODMAN_EXTENSION_ASSETS}/podman-installer-windows-arm64.exe`);
context.packager.config.extraResources.push(`${PODMAN_EXTENSION_ASSETS}/podman-installer-windows-arm64.msi`);
context.packager.config.extraResources.push(`${PODMAN_EXTENSION_ASSETS}/podman-image-arm64.zst`);
}
}

View file

@ -33,10 +33,10 @@ const mockedPodman5 = {
version: 'v5.0.0',
arch: {
x64: {
fileName: 'podman-installer-windows-amd64.exe',
fileName: 'podman-installer-windows-amd64.msi',
},
arm64: {
fileName: 'podman-installer-windows-arm64.exe',
fileName: 'podman-installer-windows-arm64.msi',
},
},
},
@ -231,13 +231,13 @@ describe('windows platform', () => {
// check called with the correct parameters
expect(downloadAndCheckShaSpy).toHaveBeenCalledWith(
expect.stringContaining(`v${podman5JSON.version}`),
expect.stringContaining('podman-installer-windows-amd64.exe'),
'podman-installer-windows-amd64.exe',
expect.stringContaining('podman-installer-windows-amd64.msi'),
'podman-installer-windows-amd64.msi',
);
expect(downloadAndCheckShaSpy).toHaveBeenCalledWith(
expect.stringContaining(`v${podman5JSON.version}`),
expect.stringContaining('podman-installer-windows-arm64.exe'),
'podman-installer-windows-arm64.exe',
expect.stringContaining('podman-installer-windows-arm64.msi'),
'podman-installer-windows-arm64.msi',
);
});
@ -262,16 +262,16 @@ describe('windows platform', () => {
expect(downloadAndCheckShaSpy).toHaveBeenNthCalledWith(
1,
'v5.0.0',
'podman-installer-windows-amd64.exe',
'podman-installer-windows-amd64.exe',
'podman-installer-windows-amd64.msi',
'podman-installer-windows-amd64.msi',
);
// check called with the correct parameters for arm64 installer
expect(downloadAndCheckShaSpy).toHaveBeenNthCalledWith(
2,
'v5.0.0',
'podman-installer-windows-arm64.exe',
'podman-installer-windows-arm64.exe',
'podman-installer-windows-arm64.msi',
'podman-installer-windows-arm64.msi',
);
// check no airgap download

View file

@ -105,13 +105,13 @@ export class PodmanDownload {
this.#artifactsToDownload.push({
version: tagVersion,
downloadName: downloadNameAmd64,
artifactName: 'podman-installer-windows-amd64.exe',
artifactName: 'podman-installer-windows-amd64.msi',
});
const downloadNameArm64 = podmanJSON.platform.win32.arch.arm64.fileName;
this.#artifactsToDownload.push({
version: tagVersion,
downloadName: downloadNameArm64,
artifactName: 'podman-installer-windows-arm64.exe',
artifactName: 'podman-installer-windows-arm64.msi',
});
} else if (this.#platform === 'darwin') {
const tagVersion = podmanJSON.platform.darwin.version;

View file

@ -1,5 +1,5 @@
/*********************************************************************
* Copyright (C) 2025 Red Hat, Inc.
* Copyright (C) 2025-2026 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -28,7 +28,7 @@ import { getAssetsFolder } from '/@/utils/util';
import { WinInstaller } from './win-installer';
vi.mock('node:fs');
vi.mock(import('node:fs'));
const extensionContext = {
subscriptions: [],
@ -41,7 +41,10 @@ const progress = {
const mockTelemetryLogger = {} as TelemetryLogger;
const mockWinPlatform = {} as WinPlatform;
const legacyInstaller = {} as PodmanWindowsLegacyInstaller;
const legacyInstaller = {
isInstalled: vi.fn(),
uninstall: vi.fn(),
} as unknown as PodmanWindowsLegacyInstaller;
vi.mock(import('/@/utils/util'), () => ({
getAssetsFolder: vi.fn(),
@ -101,3 +104,15 @@ test('expect update on windows to throw error if non zero exit code', async () =
expect(result).toBeFalsy();
expect(extensionApi.window.showErrorMessage).toHaveBeenCalled();
});
test('expect update to uninstall legacy installer if detected', async () => {
vi.mocked(legacyInstaller.isInstalled).mockResolvedValue(true);
vi.mocked(existsSync).mockReturnValue(true);
vi.mocked(readdirSync).mockReturnValue([]);
const result = await installer.update();
expect(result).toBeTruthy();
expect(legacyInstaller.uninstall).toHaveBeenCalledOnce();
});

View file

@ -62,7 +62,11 @@ export class WinInstaller extends BaseInstaller {
return this.winPlatform.getPreflightChecks();
}
update(): Promise<boolean> {
async update(): Promise<boolean> {
if (await this.legacyPodmanInstaller.isInstalled()) {
await this.legacyPodmanInstaller.uninstall();
}
return this.install();
}
@ -77,7 +81,8 @@ export class WinInstaller extends BaseInstaller {
try {
if (fs.existsSync(setupPath)) {
try {
await processAPI.exec(setupPath, ['/install', '/norestart']);
progress.report({ message: `Installing ${fileName}` });
await processAPI.exec('msiexec', ['/package', setupPath]);
progress.report({ increment: 80 });
window.showNotification({ body: 'Podman is successfully installed.' });
} catch (err) {

View file

@ -8,10 +8,10 @@
"version": "v5.7.1",
"arch": {
"x64": {
"fileName": "podman-installer-windows-amd64.exe"
"fileName": "podman-installer-windows-amd64.msi"
},
"arm64": {
"fileName": "podman-installer-windows-arm64.exe"
"fileName": "podman-installer-windows-arm64.msi"
}
}
},

View file

@ -18,6 +18,8 @@
import type { ChildProcess, ChildProcessWithoutNullStreams } from 'node:child_process';
import { spawn } from 'node:child_process';
import { homedir, platform } from 'node:os';
import { delimiter, join } from 'node:path';
import type { Readable } from 'node:stream';
import * as sudo from '@expo/sudo-prompt';
@ -577,35 +579,52 @@ describe('getInstallationPath', () => {
process.env['PATH'] = originalPath;
});
test('should return the installation path for Windows', () => {
vi.spyOn(util, 'isWindows').mockImplementation(() => true);
vi.spyOn(util, 'isMac').mockImplementation(() => false);
process.env['PATH'] = '';
describe(
'windows',
{
// as the getInstallationPath is using `node:path` and it is platform specific
// we cannot assert on non-windows platforms properly
skip: platform() !== 'win32',
},
() => {
beforeEach(() => {
vi.spyOn(util, 'isWindows').mockImplementation(() => true);
vi.spyOn(util, 'isMac').mockImplementation(() => false);
});
const path = getInstallationPath();
test('should return the installation paths for Windows', () => {
process.env['PATH'] = '';
expect(path).toBe('c:\\Program Files\\RedHat\\Podman;');
});
const path = getInstallationPath();
test('should return the installation path for Windows with pre-filled PATH', () => {
vi.spyOn(util, 'isWindows').mockImplementation(() => true);
vi.spyOn(util, 'isMac').mockImplementation(() => false);
process.env['PATH'] = 'c:\\Local';
const parts = path.split(delimiter);
expect(parts).toContain(join(homedir(), 'AppData', 'Local', 'Programs', 'Podman'));
expect(parts).toContain('c:\\Program Files\\RedHat\\Podman');
});
const path = getInstallationPath();
test('should return the installation paths for Windows with pre-filled PATH', () => {
process.env['PATH'] = 'c:\\Local';
expect(path).toBe('c:\\Program Files\\RedHat\\Podman;c:\\Local');
});
const path = getInstallationPath();
test('should return the installation path for Windows with defined param', () => {
vi.spyOn(util, 'isWindows').mockImplementation(() => true);
vi.spyOn(util, 'isMac').mockImplementation(() => false);
process.env['PATH'] = 'c:\\Local';
const parts = path.split(delimiter);
expect(parts).toContain(join(homedir(), 'AppData', 'Local', 'Programs', 'Podman'));
expect(parts).toContain('c:\\Program Files\\RedHat\\Podman');
expect(parts).toContain('c:\\Local');
});
const path = getInstallationPath('c:\\Directory');
test('should return the installation paths for Windows with defined param', () => {
process.env['PATH'] = 'c:\\Local';
expect(path).toBe('c:\\Program Files\\RedHat\\Podman;c:\\Directory');
});
const path = getInstallationPath('c:\\Directory');
const parts = path.split(delimiter);
expect(parts).toContain(join(homedir(), 'AppData', 'Local', 'Programs', 'Podman'));
expect(parts).toContain('c:\\Program Files\\RedHat\\Podman');
expect(parts).toContain('c:\\Directory');
});
},
);
test('should return the installation path for macOS', () => {
vi.spyOn(util, 'isWindows').mockImplementation(() => false);

View file

@ -18,6 +18,8 @@
import type { ChildProcessWithoutNullStreams } from 'node:child_process';
import { spawn } from 'node:child_process';
import { homedir } from 'node:os';
import { delimiter, join } from 'node:path';
import * as sudo from '@expo/sudo-prompt';
import type { RunError, RunOptions, RunResult } from '@podman-desktop/api';
@ -248,7 +250,11 @@ export function getInstallationPath(envPATH?: string): string {
envPATH ??= process.env['PATH'];
if (isWindows()) {
return `c:\\Program Files\\RedHat\\Podman;${envPATH}`;
return [
join(homedir(), 'AppData', 'Local', 'Programs', 'Podman'),
'c:\\Program Files\\RedHat\\Podman',
envPATH ?? '',
].join(delimiter);
} else if (isMac()) {
if (!envPATH) {
return macosExtraPath;

View file

@ -64,7 +64,7 @@ test.describe.serial('Podman installer integration in Podman Desktop', { tag: '@
await playExpect(podmanCliNotFoundText).toBeVisible();
});
test('Podman installer artifacts are present in local assets storage', async ({ runner }) => {
const fileFormatRegexp = isWindows ? 'exe' : 'pkg';
const fileFormatRegexp = isWindows ? 'msi' : 'pkg';
// x64 = amd64 for both windows and mac, arm64 = arm64 for win, and aarch64 for mac
const archPart = process.arch === 'x64' ? 'amd64' : process.arch === 'arm64' ? (isMac ? 'aarch64' : 'arm64') : null;
playExpect(archPart, { message: `Unsupported architecture: ${process.arch}` }).not.toBeNull();