diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index 362559c2e..550b086d0 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -450,19 +450,22 @@ async function getPodmanArgs(params: DockerResolverParameters, config: DevContai return []; } -// Convert a --mount string (e.g., "type=bind,source=/a,target=/b,consistency=cached") to -v syntax for wslc. -function convertMountToVolume(mountStr: string): string[] { +// Convert a --mount string (e.g., "type=bind,source=/a,target=/b,consistency=cached,readonly") +// to -v syntax for wslc. Options other than source/target/readonly are dropped. +export function convertMountToVolume(mountStr: string): string[] { const parts = new Map(mountStr.split(',').map(p => { const eq = p.indexOf('='); return eq === -1 ? [p, ''] : [p.substring(0, eq), p.substring(eq + 1)]; })); const source = parts.get('source') || parts.get('src') || ''; const target = parts.get('target') || parts.get('dst') || parts.get('destination') || ''; + const readonlyValue = parts.has('readonly') ? parts.get('readonly') : parts.get('ro'); + const readonlySuffix = readonlyValue !== undefined && !/^(false|0)$/i.test(readonlyValue) ? ':ro' : ''; if (source && target) { - return ['-v', `${source}:${target}`]; + return ['-v', `${source}:${target}${readonlySuffix}`]; } if (target) { - return ['-v', target]; + return ['-v', `${target}${readonlySuffix}`]; } // Fallback: pass as --mount and let the runtime handle it. return ['--mount', mountStr]; diff --git a/src/test/singleContainer.test.ts b/src/test/singleContainer.test.ts new file mode 100644 index 000000000..636d5bdf1 --- /dev/null +++ b/src/test/singleContainer.test.ts @@ -0,0 +1,65 @@ +import { assert } from 'chai'; +import { convertMountToVolume } from '../spec-node/singleContainer'; + +describe('convertMountToVolume (wslc -v syntax)', () => { + + it('converts a bind mount with source and target', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b'), + ['-v', '/a:/b']); + }); + + it('preserves the readonly flag as :ro', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,readonly'), + ['-v', '/a:/b:ro']); + }); + + it('preserves readonly=true as :ro', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,readonly=true'), + ['-v', '/a:/b:ro']); + }); + + it('preserves the ro shorthand as :ro', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,ro'), + ['-v', '/a:/b:ro']); + }); + + it('ignores readonly=false', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,readonly=false'), + ['-v', '/a:/b']); + }); + + it('drops the consistency option', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=/a,target=/b,consistency=cached'), + ['-v', '/a:/b']); + }); + + it('converts a named volume mount', () => { + assert.deepEqual( + convertMountToVolume('type=volume,source=vol,target=/b,readonly'), + ['-v', 'vol:/b:ro']); + }); + + it('converts a Windows source path with a readonly flag', () => { + assert.deepEqual( + convertMountToVolume('type=bind,source=C:\\some\\folder,target=/b,readonly'), + ['-v', 'C:\\some\\folder:/b:ro']); + }); + + it('converts a target-only mount to an anonymous volume', () => { + assert.deepEqual( + convertMountToVolume('type=volume,target=/b'), + ['-v', '/b']); + }); + + it('falls back to --mount when no target is present', () => { + assert.deepEqual( + convertMountToVolume('type=tmpfs'), + ['--mount', 'type=tmpfs']); + }); +});