Skip to content

Commit 508e3c6

Browse files
authored
normalize feature ID earlier, and keep raw around
1 parent a39ea29 commit 508e3c6

File tree

7 files changed

+156
-137
lines changed

7 files changed

+156
-137
lines changed

src/spec-configuration/configuration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ export interface HostRequirements {
3434
}
3535

3636
export interface DevContainerFeature {
37-
userFeatureId: string;
37+
rawUserFeatureId: string;
38+
normalizedUserFeatureId: string;
3839
options: boolean | string | Record<string, boolean | string | undefined>;
3940
}
4041

src/spec-configuration/containerFeaturesConfiguration.ts

Lines changed: 54 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
526526
const workspaceRoot = params.cwd;
527527
output.write(`workspace root: ${workspaceRoot}`, LogLevel.Trace);
528528

529-
const userFeatures = updateDeprecatedFeaturesIntoOptions(normalizedUserFeaturesToArray(config, additionalFeatures), output);
529+
const userFeatures = updateDeprecatedFeaturesIntoOptions(userFeaturesToArray(output, config, additionalFeatures), output);
530530
if (!userFeatures) {
531531
return undefined;
532532
}
@@ -547,8 +547,8 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
547547

548548
const lockfile = await readLockfile(config);
549549

550-
const processFeature = async (_userFeature: DevContainerFeature) => {
551-
return await processFeatureIdentifier(params, configPath, workspaceRoot, _userFeature, lockfile);
550+
const processFeature = async (f: { userFeature: DevContainerFeature }) => {
551+
return await processFeatureIdentifier(params, configPath, workspaceRoot, f.userFeature, lockfile);
552552
};
553553

554554
output.write('--- Processing User Features ----', LogLevel.Trace);
@@ -575,7 +575,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
575575
export async function loadVersionInfo(params: ContainerFeatureInternalParams, config: DevContainerConfig) {
576576
const { output } = params;
577577

578-
const userFeatures = updateDeprecatedFeaturesIntoOptions(normalizedUserFeaturesToArray(config), output);
578+
const userFeatures = updateDeprecatedFeaturesIntoOptions(userFeaturesToArray(output, config), output);
579579
if (!userFeatures) {
580580
return { features: {} };
581581
}
@@ -585,7 +585,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co
585585
const features: Record<string, any> = {};
586586

587587
await Promise.all(userFeatures.map(async userFeature => {
588-
const userFeatureId = userFeature.userFeatureId;
588+
const userFeatureId = userFeature.rawUserFeatureId;
589589
const updatedFeatureId = getBackwardCompatibleFeatureId(output, userFeatureId);
590590
const featureRef = getRef(output, updatedFeatureId);
591591
if (featureRef) {
@@ -644,7 +644,12 @@ async function prepareOCICache(dstFolder: string) {
644644
return ociCacheDir;
645645
}
646646

647-
export function normalizedUserFeaturesToArray(config: DevContainerConfig, additionalFeatures?: Record<string, string | boolean | Record<string, string | boolean>>): DevContainerFeature[] | undefined {
647+
export function normalizeUserFeatureIdentifier(output: Log, userFeatureId: string): string {
648+
return getBackwardCompatibleFeatureId(output, userFeatureId).toLowerCase();
649+
}
650+
651+
652+
export function userFeaturesToArray(output: Log, config: DevContainerConfig, additionalFeatures?: Record<string, string | boolean | Record<string, string | boolean>>): DevContainerFeature[] | undefined {
648653
if (!Object.keys(config.features || {}).length && !Object.keys(additionalFeatures || {}).length) {
649654
return undefined;
650655
}
@@ -653,26 +658,28 @@ export function normalizedUserFeaturesToArray(config: DevContainerConfig, additi
653658
const keys = new Set<string>();
654659

655660
if (config.features) {
656-
for (const rawKey of Object.keys(config.features)) {
657-
const userFeatureKeyNormalized = rawKey.toLowerCase();
658-
const userFeatureValue = config.features[rawKey];
661+
for (const rawUserFeatureId of Object.keys(config.features)) {
662+
const normalizedUserFeatureId = normalizeUserFeatureIdentifier(output, rawUserFeatureId);
663+
const userFeatureValue = config.features[rawUserFeatureId];
659664
const feature: DevContainerFeature = {
660-
userFeatureId: userFeatureKeyNormalized,
665+
rawUserFeatureId,
666+
normalizedUserFeatureId,
661667
options: userFeatureValue
662668
};
663669
userFeatures.push(feature);
664-
keys.add(userFeatureKeyNormalized);
670+
keys.add(normalizedUserFeatureId);
665671
}
666672
}
667673

668674
if (additionalFeatures) {
669-
for (const rawKey of Object.keys(additionalFeatures)) {
670-
const userFeatureKeyNormalized = rawKey.toLowerCase();
675+
for (const rawUserFeatureId of Object.keys(additionalFeatures)) {
676+
const normalizedUserFeatureId = normalizeUserFeatureIdentifier(output, rawUserFeatureId);
671677
// add the additional feature if it hasn't already been added from the config features
672-
if (!keys.has(userFeatureKeyNormalized)) {
673-
const userFeatureValue = additionalFeatures[rawKey];
678+
if (!keys.has(normalizedUserFeatureId)) {
679+
const userFeatureValue = additionalFeatures[rawUserFeatureId];
674680
const feature: DevContainerFeature = {
675-
userFeatureId: userFeatureKeyNormalized,
681+
rawUserFeatureId,
682+
normalizedUserFeatureId,
676683
options: userFeatureValue
677684
};
678685
userFeatures.push(feature);
@@ -712,11 +719,11 @@ export function updateDeprecatedFeaturesIntoOptions(userFeatures: DevContainerFe
712719

713720
const newFeaturePath = 'ghcr.io/devcontainers/features';
714721
const versionBackwardComp = '1';
715-
for (const update of userFeatures.filter(feature => deprecatedFeaturesIntoOptions[feature.userFeatureId])) {
716-
const { mapTo, withOptions } = deprecatedFeaturesIntoOptions[update.userFeatureId];
717-
output.write(`(!) WARNING: Using the deprecated '${update.userFeatureId}' Feature. It is now part of the '${mapTo}' Feature. See https://github.com/devcontainers/features/tree/main/src/${mapTo}#options for the updated Feature.`, LogLevel.Warning);
722+
for (const update of userFeatures.filter(feature => deprecatedFeaturesIntoOptions[feature.rawUserFeatureId])) {
723+
const { mapTo, withOptions } = deprecatedFeaturesIntoOptions[update.rawUserFeatureId];
724+
output.write(`(!) WARNING: Using the deprecated '${update.rawUserFeatureId}' Feature. It is now part of the '${mapTo}' Feature. See https://github.com/devcontainers/features/tree/main/src/${mapTo}#options for the updated Feature.`, LogLevel.Warning);
718725
const qualifiedMapToId = `${newFeaturePath}/${mapTo}`;
719-
let userFeature = userFeatures.find(feature => feature.userFeatureId === mapTo || feature.userFeatureId === qualifiedMapToId || feature.userFeatureId.startsWith(`${qualifiedMapToId}:`));
726+
let userFeature = userFeatures.find(feature => feature.rawUserFeatureId === mapTo || feature.rawUserFeatureId === qualifiedMapToId || feature.rawUserFeatureId.startsWith(`${qualifiedMapToId}:`));
720727
if (userFeature) {
721728
userFeature.options = {
722729
...(
@@ -727,14 +734,16 @@ export function updateDeprecatedFeaturesIntoOptions(userFeatures: DevContainerFe
727734
...withOptions,
728735
};
729736
} else {
737+
const rawUserFeatureId = `${qualifiedMapToId}:${versionBackwardComp}`;
730738
userFeature = {
731-
userFeatureId: `${qualifiedMapToId}:${versionBackwardComp}`,
739+
rawUserFeatureId,
740+
normalizedUserFeatureId: normalizeUserFeatureIdentifier(output, rawUserFeatureId),
732741
options: withOptions
733742
};
734743
userFeatures.push(userFeature);
735744
}
736745
}
737-
const updatedUserFeatures = userFeatures.filter(feature => !deprecatedFeaturesIntoOptions[feature.userFeatureId]);
746+
const updatedUserFeatures = userFeatures.filter(feature => !deprecatedFeaturesIntoOptions[feature.rawUserFeatureId]);
738747
return updatedUserFeatures;
739748
}
740749

@@ -821,33 +830,26 @@ export function getBackwardCompatibleFeatureId(output: Log, id: string) {
821830
export async function processFeatureIdentifier(params: CommonParams, configPath: string | undefined, _workspaceRoot: string, userFeature: DevContainerFeature, lockfile?: Lockfile): Promise<FeatureSet | undefined> {
822831
const { output } = params;
823832

824-
output.write(`* Processing feature: ${userFeature.userFeatureId}`);
825-
826-
// id referenced by the user before the automapping from old shorthand syntax to "ghcr.io/devcontainers/features"
827-
const originalUserFeatureId = userFeature.userFeatureId;
828-
// Adding backward compatibility
829-
if (!skipFeatureAutoMapping) {
830-
userFeature.userFeatureId = getBackwardCompatibleFeatureId(output, userFeature.userFeatureId);
831-
}
833+
output.write(`* Processing feature: ${userFeature.rawUserFeatureId}`);
832834

833-
const { type, manifest } = await getFeatureIdType(params, userFeature.userFeatureId, lockfile);
835+
const { type, manifest } = await getFeatureIdType(params, userFeature.normalizedUserFeatureId, lockfile);
834836

835837
// cached feature
836838
// Resolves deprecated features (fish, maven, gradle, homebrew, jupyterlab)
837839
if (type === 'local-cache') {
838840
output.write(`Cached feature found.`);
839841

840842
let feat: Feature = {
841-
id: userFeature.userFeatureId,
842-
name: userFeature.userFeatureId,
843+
id: userFeature.normalizedUserFeatureId,
844+
name: userFeature.normalizedUserFeatureId,
843845
value: userFeature.options,
844846
included: true,
845847
};
846848

847849
let newFeaturesSet: FeatureSet = {
848850
sourceInformation: {
849851
type: 'local-cache',
850-
userFeatureId: originalUserFeatureId
852+
userFeatureId: userFeature.rawUserFeatureId // Without backwards-compatible normalization
851853
},
852854
features: [feat],
853855
};
@@ -858,7 +860,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
858860
// remote tar file
859861
if (type === 'direct-tarball') {
860862
output.write(`Remote tar file found.`);
861-
const tarballUri = new URL.URL(userFeature.userFeatureId);
863+
const tarballUri = new URL.URL(userFeature.rawUserFeatureId);
862864

863865
const fullPath = tarballUri.pathname;
864866
const tarballName = fullPath.substring(fullPath.lastIndexOf('/') + 1);
@@ -879,8 +881,8 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
879881
}
880882

881883
let feat: Feature = {
882-
id: id,
883-
name: userFeature.userFeatureId,
884+
id,
885+
name: userFeature.normalizedUserFeatureId,
884886
value: userFeature.options,
885887
included: true,
886888
};
@@ -889,7 +891,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
889891
sourceInformation: {
890892
type: 'direct-tarball',
891893
tarballUri: tarballUri.toString(),
892-
userFeatureId: originalUserFeatureId
894+
userFeatureId: userFeature.normalizedUserFeatureId,
893895
},
894896
features: [feat],
895897
};
@@ -901,10 +903,8 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
901903
if (type === 'file-path') {
902904
output.write(`Local disk feature.`);
903905

904-
const id = path.basename(userFeature.userFeatureId);
905-
906906
// Fail on Absolute paths.
907-
if (path.isAbsolute(userFeature.userFeatureId)) {
907+
if (path.isAbsolute(userFeature.normalizedUserFeatureId)) {
908908
output.write('An Absolute path to a local feature is not allowed.', LogLevel.Error);
909909
return undefined;
910910
}
@@ -914,7 +914,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
914914
output.write('A local feature requires a configuration path.', LogLevel.Error);
915915
return undefined;
916916
}
917-
const featureFolderPath = path.join(path.dirname(configPath), userFeature.userFeatureId);
917+
const featureFolderPath = path.join(path.dirname(configPath), userFeature.rawUserFeatureId); // OS Path may be case-sensitive
918918

919919
// Ensure we aren't escaping .devcontainer folder
920920
const parent = path.join(_workspaceRoot, '.devcontainer');
@@ -926,14 +926,13 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
926926
return undefined;
927927
}
928928

929-
output.write(`Resolved: ${userFeature.userFeatureId} -> ${featureFolderPath}`, LogLevel.Trace);
929+
output.write(`Resolved: ${userFeature.rawUserFeatureId} -> ${featureFolderPath}`, LogLevel.Trace);
930930

931931
// -- All parsing and validation steps complete at this point.
932932

933-
output.write(`Parsed feature id: ${id}`, LogLevel.Trace);
934933
let feat: Feature = {
935-
id,
936-
name: userFeature.userFeatureId,
934+
id: path.basename(userFeature.normalizedUserFeatureId),
935+
name: userFeature.normalizedUserFeatureId,
937936
value: userFeature.options,
938937
included: true,
939938
};
@@ -942,7 +941,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
942941
sourceInformation: {
943942
type: 'file-path',
944943
resolvedFilePath: featureFolderPath,
945-
userFeatureId: originalUserFeatureId
944+
userFeatureId: userFeature.normalizedUserFeatureId
946945
},
947946
features: [feat],
948947
};
@@ -952,19 +951,19 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
952951

953952
// (6) Oci Identifier
954953
if (type === 'oci' && manifest) {
955-
return tryGetOCIFeatureSet(output, userFeature.userFeatureId, userFeature.options, manifest, originalUserFeatureId);
954+
return tryGetOCIFeatureSet(output, userFeature.normalizedUserFeatureId, userFeature.options, manifest, userFeature.rawUserFeatureId);
956955
}
957956

958957
output.write(`Github feature.`);
959958
// Github repository source.
960959
let version = 'latest';
961-
let splitOnAt = userFeature.userFeatureId.split('@');
960+
let splitOnAt = userFeature.normalizedUserFeatureId.split('@');
962961
if (splitOnAt.length > 2) {
963962
output.write(`Parse error. Use the '@' symbol only to designate a version tag.`, LogLevel.Error);
964963
return undefined;
965964
}
966965
if (splitOnAt.length === 2) {
967-
output.write(`[${userFeature.userFeatureId}] has version ${splitOnAt[1]}`, LogLevel.Trace);
966+
output.write(`[${userFeature.normalizedUserFeatureId}] has version ${splitOnAt[1]}`, LogLevel.Trace);
968967
version = splitOnAt[1];
969968
}
970969

@@ -975,7 +974,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
975974
// eg: <publisher>/<feature-set>/<feature>
976975
if (splitOnSlash.length !== 3 || splitOnSlash.some(x => x === '') || !allowedFeatureIdRegex.test(splitOnSlash[2])) {
977976
// This is the final fallback. If we end up here, we weren't able to resolve the Feature
978-
output.write(`Could not resolve Feature '${userFeature.userFeatureId}'. Ensure the Feature is published and accessible from your current environment.`, LogLevel.Error);
977+
output.write(`Could not resolve Feature '${userFeature.normalizedUserFeatureId}'. Ensure the Feature is published and accessible from your current environment.`, LogLevel.Error);
979978
return undefined;
980979
}
981980
const owner = splitOnSlash[0];
@@ -984,12 +983,12 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
984983

985984
let feat: Feature = {
986985
id: id,
987-
name: userFeature.userFeatureId,
986+
name: userFeature.normalizedUserFeatureId,
988987
value: userFeature.options,
989988
included: true,
990989
};
991990

992-
const userFeatureIdWithoutVersion = originalUserFeatureId.split('@')[0];
991+
const userFeatureIdWithoutVersion = userFeature.normalizedUserFeatureId.split('@')[0];
993992
if (version === 'latest') {
994993
let newFeaturesSet: FeatureSet = {
995994
sourceInformation: {
@@ -999,7 +998,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
999998
owner,
1000999
repo,
10011000
isLatest: true,
1002-
userFeatureId: originalUserFeatureId,
1001+
userFeatureId: userFeature.normalizedUserFeatureId,
10031002
userFeatureIdWithoutVersion
10041003
},
10051004
features: [feat],
@@ -1016,7 +1015,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
10161015
repo,
10171016
tag: version,
10181017
isLatest: false,
1019-
userFeatureId: originalUserFeatureId,
1018+
userFeatureId: userFeature.normalizedUserFeatureId,
10201019
userFeatureIdWithoutVersion
10211020
},
10221021
features: [feat],

0 commit comments

Comments
 (0)