Skip to content

Commit db95abf

Browse files
committed
fix: Fixes for docker and uv. Added global parameter for uv
1 parent 3fcb30c commit db95abf

6 files changed

Lines changed: 91 additions & 8 deletions

File tree

src/resources/docker/docker.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ export class DockerResource extends Resource<DockerConfig> {
175175
{ requiresRoot: true }
176176
);
177177
await $.spawn(
178-
'curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg',
178+
'curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o --batch --yes /etc/apt/keyrings/docker.gpg',
179179
{ requiresRoot: true }
180180
);
181181
await $.spawn(
@@ -189,7 +189,7 @@ export class DockerResource extends Resource<DockerConfig> {
189189
const distro = await this.getDebianDistro($);
190190

191191
await $.spawn(
192-
`echo "deb [arch=${arch} signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${distro} $(lsb_release -cs) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null`,
192+
`bash -c 'echo "deb [arch=${arch} signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/${distro} $(lsb_release -cs 2>/dev/null) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null'`,
193193
{ requiresRoot: true }
194194
);
195195

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import { getPty, ParameterSetting, SpawnStatus, StatefulParameter } from '@codifycli/plugin-core';
2+
3+
import { UvConfig } from './uv.js';
4+
5+
/**
6+
* Manages the global default Python version exposed on PATH via uv.
7+
*
8+
* `uv python install <version> --default` installs unversioned `python` and
9+
* `python3` symlinks into ~/.local/bin, making that version the system-wide
10+
* default outside of any project context.
11+
*
12+
* To detect the current default, we read the symlink at ~/.local/bin/python
13+
* and parse the cpython version string from the target path.
14+
*/
15+
export class UvGlobalParameter extends StatefulParameter<UvConfig, string> {
16+
getSettings(): ParameterSetting {
17+
return {
18+
type: 'version',
19+
};
20+
}
21+
22+
override async refresh(): Promise<string | null> {
23+
const $ = getPty();
24+
25+
// Check if ~/.local/bin/python exists and points to a uv-managed interpreter.
26+
// `readlink` resolves the symlink target; if it contains cpython we know it
27+
// was installed by uv with --default.
28+
const { status, data } = await $.spawnSafe('readlink ~/.local/bin/python');
29+
if (status === SpawnStatus.ERROR || !data.trim()) {
30+
return null;
31+
}
32+
33+
// Symlink target is a path like .../cpython-3.12.3-.../bin/python3.12
34+
const match = data.trim().match(/cpython-(\d+\.\d+(?:\.\d+)?)/);
35+
36+
return match ? match[1] : null;
37+
}
38+
39+
override async add(version: string): Promise<void> {
40+
const $ = getPty();
41+
await $.spawn(`uv python install ${version} --default`, { interactive: true });
42+
}
43+
44+
override async modify(newVersion: string): Promise<void> {
45+
const $ = getPty();
46+
await $.spawn(`uv python install ${newVersion} --default`, { interactive: true });
47+
}
48+
49+
override async remove(_version: string): Promise<void> {
50+
const $ = getPty();
51+
// uv has no "unset default" command. Remove the unversioned symlinks that
52+
// --default created in ~/.local/bin so `python` / `python3` no longer
53+
// resolve to this uv-managed interpreter. The versioned binary is left
54+
// intact because it may still be listed in pythonVersions.
55+
await $.spawnSafe('rm -f ~/.local/bin/python ~/.local/bin/python3');
56+
}
57+
}

src/resources/python/uv/python-versions-parameter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ function parseInstalledPythonVersions(output: string): string[] {
5050
return output
5151
.split('\n')
5252
.map((line) => {
53-
const match = line.match(/cpython-(\d+\.\d+\.\d+)/);
53+
const match = line.match(/cpython-(\d+\.\d+(?:\.\d+)?)/);
5454
return match ? match[1] : null;
5555
})
5656
.filter((v): v is string => v !== null);

src/resources/python/uv/tools-parameter.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@ export class UvToolsParameter extends ArrayStatefulParameter<UvConfig, string> {
2727

2828
override async addItem(tool: string): Promise<void> {
2929
const $ = getPty();
30-
await $.spawn(`uv tool install ${tool}`, { interactive: true });
30+
await $.spawnSafe(`uv tool install --force ${tool}`, { interactive: true });
3131
}
3232

3333
override async removeItem(tool: string): Promise<void> {
3434
const $ = getPty();
35-
await $.spawn(`uv tool uninstall ${tool}`, { interactive: true });
35+
await $.spawnSafe(`uv tool uninstall ${tool}`, { interactive: true });
3636
}
3737
}
3838

src/resources/python/uv/uv.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import fs from 'node:fs/promises';
1313
import os from 'node:os';
1414
import path from 'node:path';
1515

16+
import { UvGlobalParameter } from './global-parameter.js';
1617
import { UvPythonVersionsParameter } from './python-versions-parameter.js';
1718
import { UvToolsParameter } from './tools-parameter.js';
1819

@@ -24,6 +25,10 @@ const schema = z.object({
2425
.array(z.string())
2526
.describe('Python versions to install via uv (e.g. ["3.12", "3.11"])')
2627
.optional(),
28+
global: z
29+
.string()
30+
.describe('Python version to set as the global default (exposes `python` and `python3` on PATH via --default flag)')
31+
.optional(),
2732
tools: z
2833
.array(z.string())
2934
.describe('Global CLI tools to install via uv tool install (e.g. ["ruff", "black"])')
@@ -41,19 +46,21 @@ const defaultConfig: Partial<UvConfig> = {
4146

4247
const examplePython: ExampleConfig = {
4348
title: 'Install uv with Python versions',
44-
description: 'Install uv and pin one or more Python versions for use across projects.',
49+
description: 'Install uv, pin one or more Python versions, and set one as the global default accessible as `python` on PATH.',
4550
configs: [{
4651
type: 'uv',
4752
pythonVersions: ['3.12', '3.11'],
53+
global: '3.12',
4854
}]
4955
}
5056

5157
const exampleWithTools: ExampleConfig = {
5258
title: 'Install uv with Python and global tools',
53-
description: 'Install uv, pin a Python version, and install commonly used global CLI tools like ruff and black.',
59+
description: 'Install uv, set a global default Python, and install commonly used global CLI tools like ruff and black.',
5460
configs: [{
5561
type: 'uv',
5662
pythonVersions: ['3.12'],
63+
global: '3.12',
5764
tools: ['ruff', 'black', 'httpie'],
5865
}]
5966
}
@@ -71,7 +78,8 @@ export class UvResource extends Resource<UvConfig> {
7178
schema,
7279
parameterSettings: {
7380
pythonVersions: { type: 'stateful', definition: new UvPythonVersionsParameter(), order: 1 },
74-
tools: { type: 'stateful', definition: new UvToolsParameter(), order: 2 },
81+
global: { type: 'stateful', definition: new UvGlobalParameter(), order: 2 },
82+
tools: { type: 'stateful', definition: new UvToolsParameter(), order: 3 },
7583
},
7684
dependencies: [...(Utils.isMacOS() ? ['homebrew'] : [])],
7785
};

test/python/uv.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,24 @@ describe('uv resource integration tests', () => {
2525
});
2626
});
2727

28+
it('Installs uv and sets a global default Python', { timeout: 300_000 }, async () => {
29+
await PluginTester.fullTest(pluginPath, [
30+
{
31+
type: 'uv',
32+
pythonVersions: ['3.12'],
33+
global: '3.12',
34+
},
35+
], {
36+
validateApply: async () => {
37+
const { data: version } = await testSpawn('python --version');
38+
expect(version).toContain('3.12');
39+
},
40+
validateDestroy: async () => {
41+
expect(await testSpawn('uv --version')).toMatchObject({ status: SpawnStatus.ERROR });
42+
},
43+
});
44+
});
45+
2846
it('Installs uv and manages global tools', { timeout: 300_000 }, async () => {
2947
await PluginTester.fullTest(pluginPath, [
3048
{

0 commit comments

Comments
 (0)