Skip to content

Commit c654399

Browse files
committed
Normalize drive letter to lowercase on Windows to match VSCode
1 parent 39685cf commit c654399

3 files changed

Lines changed: 115 additions & 9 deletions

File tree

src/spec-node/featuresCLI/testCommandImpl.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { CLIHost } from '../../spec-common/cliHost';
66
import { launch, ProvisionOptions, createDockerParams } from '../devContainers';
77
import { doExec } from '../devContainersSpecCLI';
88
import { LaunchResult, staticExecParams, staticProvisionParams, testLibraryScript } from './utils';
9-
import { DockerResolverParameters } from '../utils';
9+
import { DockerResolverParameters, normalizeDevContainerLabelPath } from '../utils';
1010
import { DevContainerConfig } from '../../spec-configuration/configuration';
1111
import { FeaturesTestCommandInput } from './test';
1212
import { cpDirectoryLocal, rmLocal } from '../../spec-utils/pfs';
@@ -546,7 +546,8 @@ async function launchProject(params: DockerResolverParameters, workspaceFolder:
546546
const { common } = params;
547547
let response = {} as LaunchResult;
548548

549-
const idLabels = [`devcontainer.local_folder=${workspaceFolder}`, `devcontainer.is_test_run=true`];
549+
const normalizedWorkspaceFolder = normalizeDevContainerLabelPath(process.platform, workspaceFolder);
550+
const idLabels = [`devcontainer.local_folder=${normalizedWorkspaceFolder}`, `devcontainer.is_test_run=true`];
550551
const options: ProvisionOptions = {
551552
...staticProvisionParams,
552553
workspaceFolder,

src/spec-node/utils.ts

Lines changed: 83 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { CommonDevContainerConfig, ContainerProperties, getContainerProperties,
1515
import { Workspace } from '../spec-utils/workspaces';
1616
import { URI } from 'vscode-uri';
1717
import { ShellServer } from '../spec-common/shellServer';
18-
import { inspectContainer, inspectImage, getEvents, ContainerDetails, DockerCLIParameters, dockerExecFunction, dockerPtyCLI, dockerPtyExecFunction, toDockerImageName, DockerComposeCLI, ImageDetails, dockerCLI, removeContainer } from '../spec-shutdown/dockerUtils';
18+
import { inspectContainer, inspectContainers, inspectImage, getEvents, listContainers, ContainerDetails, DockerCLIParameters, dockerExecFunction, dockerPtyCLI, dockerPtyExecFunction, toDockerImageName, DockerComposeCLI, ImageDetails, dockerCLI, removeContainer } from '../spec-shutdown/dockerUtils';
1919
import { getRemoteWorkspaceFolder } from './dockerCompose';
2020
import { findGitRootFolder } from '../spec-common/git';
2121
import { parentURI, uriToFsPath } from '../spec-configuration/configurationCommonUtils';
@@ -614,21 +614,99 @@ export function getEmptyContextFolder(common: ResolverParameters) {
614614
return common.cliHost.path.join(common.persistedFolder, 'empty-folder');
615615
}
616616

617+
export function normalizeDevContainerLabelPath(platform: NodeJS.Platform, value: string): string {
618+
if (platform !== 'win32') {
619+
return value;
620+
}
621+
622+
// Normalize separators and dot segments, then explicitly lowercase the drive
623+
// letter because devcontainer.local_folder / devcontainer.config_file labels
624+
// should compare case-insensitively on Windows.
625+
const normalized = path.win32.normalize(value);
626+
if (normalized.length >= 2 && normalized[1] === ':') {
627+
return normalized[0].toLowerCase() + normalized.slice(1);
628+
}
629+
630+
return normalized;
631+
}
632+
633+
async function findDevContainerByNormalizedLabels(params: DockerResolverParameters | DockerCLIParameters, normalizedWorkspaceFolder: string, normalizedConfigFile: string) {
634+
if (process.platform !== 'win32') {
635+
return undefined;
636+
}
637+
638+
const ids = await listContainers(params, true, [hostFolderLabel]);
639+
if (!ids.length) {
640+
return undefined;
641+
}
642+
643+
const details = await inspectContainers(params, ids);
644+
return details
645+
.filter(container => container.State.Status !== 'removing')
646+
.find(container => {
647+
const labels = container.Config.Labels || {};
648+
const containerWorkspaceFolder = labels[hostFolderLabel];
649+
if (!containerWorkspaceFolder || normalizeDevContainerLabelPath('win32', containerWorkspaceFolder) !== normalizedWorkspaceFolder) {
650+
return false;
651+
}
652+
653+
const containerConfigFile = labels[configFileLabel];
654+
return !!containerConfigFile
655+
&& normalizeDevContainerLabelPath('win32', containerConfigFile) === normalizedConfigFile;
656+
});
657+
}
658+
659+
async function findLegacyDevContainerByNormalizedWorkspaceFolder(params: DockerResolverParameters | DockerCLIParameters, normalizedWorkspaceFolder: string) {
660+
if (process.platform !== 'win32') {
661+
return undefined;
662+
}
663+
664+
const ids = await listContainers(params, true, [hostFolderLabel]);
665+
if (!ids.length) {
666+
return undefined;
667+
}
668+
669+
const details = await inspectContainers(params, ids);
670+
return details
671+
.filter(container => container.State.Status !== 'removing')
672+
.find(container => {
673+
const labels = container.Config.Labels || {};
674+
const containerWorkspaceFolder = labels[hostFolderLabel];
675+
if (!containerWorkspaceFolder) {
676+
return false;
677+
}
678+
679+
return normalizeDevContainerLabelPath('win32', containerWorkspaceFolder) === normalizedWorkspaceFolder;
680+
});
681+
}
682+
617683
export async function findContainerAndIdLabels(params: DockerResolverParameters | DockerCLIParameters, containerId: string | undefined, providedIdLabels: string[] | undefined, workspaceFolder: string | undefined, configFile: string | undefined, removeContainerWithOldLabels?: boolean | string) {
618684
if (providedIdLabels) {
619685
return {
620686
container: containerId ? await inspectContainer(params, containerId) : await findDevContainer(params, providedIdLabels),
621687
idLabels: providedIdLabels,
622688
};
623689
}
690+
691+
const normalizedWorkspaceFolder = workspaceFolder ? normalizeDevContainerLabelPath(process.platform, workspaceFolder) : workspaceFolder;
692+
const normalizedConfigFile = configFile ? normalizeDevContainerLabelPath(process.platform, configFile) : configFile;
693+
const newLabels = [`${hostFolderLabel}=${normalizedWorkspaceFolder}`, `${configFileLabel}=${normalizedConfigFile}`];
694+
const oldLabels = [`${hostFolderLabel}=${normalizedWorkspaceFolder}`];
695+
624696
let container: ContainerDetails | undefined;
625697
if (containerId) {
626698
container = await inspectContainer(params, containerId);
627-
} else if (workspaceFolder && configFile) {
628-
container = await findDevContainer(params, [`${hostFolderLabel}=${workspaceFolder}`, `${configFileLabel}=${configFile}`]);
699+
} else if (normalizedWorkspaceFolder && normalizedConfigFile) {
700+
container = await findDevContainer(params, newLabels);
701+
if (!container) {
702+
container = await findDevContainerByNormalizedLabels(params, normalizedWorkspaceFolder, normalizedConfigFile);
703+
}
629704
if (!container) {
630705
// Fall back to old labels.
631-
container = await findDevContainer(params, [`${hostFolderLabel}=${workspaceFolder}`]);
706+
container = await findDevContainer(params, oldLabels);
707+
if (!container) {
708+
container = await findLegacyDevContainerByNormalizedWorkspaceFolder(params, normalizedWorkspaceFolder);
709+
}
632710
if (container) {
633711
if (container.Config.Labels?.[configFileLabel]) {
634712
// But ignore containers with new labels.
@@ -645,9 +723,7 @@ export async function findContainerAndIdLabels(params: DockerResolverParameters
645723
}
646724
return {
647725
container,
648-
idLabels: !container || container.Config.Labels?.[configFileLabel] ?
649-
[`${hostFolderLabel}=${workspaceFolder}`, `${configFileLabel}=${configFile}`] :
650-
[`${hostFolderLabel}=${workspaceFolder}`],
726+
idLabels: !container || container.Config.Labels?.[configFileLabel] ? newLabels : oldLabels,
651727
};
652728
}
653729

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
*--------------------------------------------------------------------------------------------*/
4+
5+
import { assert } from 'chai';
6+
import { normalizeDevContainerLabelPath } from '../spec-node/utils';
7+
8+
describe('normalizeDevContainerLabelPath', function () {
9+
it('lowercases Windows drive letters', function () {
10+
assert.equal(
11+
normalizeDevContainerLabelPath('win32', 'C:\\CodeBlocks\\remill'),
12+
'c:\\CodeBlocks\\remill'
13+
);
14+
});
15+
16+
it('normalizes Windows path separators', function () {
17+
assert.equal(
18+
normalizeDevContainerLabelPath('win32', 'C:/CodeBlocks/remill/.devcontainer/devcontainer.json'),
19+
'c:\\CodeBlocks\\remill\\.devcontainer\\devcontainer.json'
20+
);
21+
});
22+
23+
it('leaves non-Windows paths unchanged', function () {
24+
assert.equal(
25+
normalizeDevContainerLabelPath('linux', '/workspaces/remill'),
26+
'/workspaces/remill'
27+
);
28+
});
29+
});

0 commit comments

Comments
 (0)