@@ -526,7 +526,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
526
526
const workspaceRoot = params . cwd ;
527
527
output . write ( `workspace root: ${ workspaceRoot } ` , LogLevel . Trace ) ;
528
528
529
- const userFeatures = updateDeprecatedFeaturesIntoOptions ( normalizedUserFeaturesToArray ( config , additionalFeatures ) , output ) ;
529
+ const userFeatures = updateDeprecatedFeaturesIntoOptions ( userFeaturesToArray ( output , config , additionalFeatures ) , output ) ;
530
530
if ( ! userFeatures ) {
531
531
return undefined ;
532
532
}
@@ -547,8 +547,8 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
547
547
548
548
const lockfile = await readLockfile ( config ) ;
549
549
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 ) ;
552
552
} ;
553
553
554
554
output . write ( '--- Processing User Features ----' , LogLevel . Trace ) ;
@@ -575,7 +575,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar
575
575
export async function loadVersionInfo ( params : ContainerFeatureInternalParams , config : DevContainerConfig ) {
576
576
const { output } = params ;
577
577
578
- const userFeatures = updateDeprecatedFeaturesIntoOptions ( normalizedUserFeaturesToArray ( config ) , output ) ;
578
+ const userFeatures = updateDeprecatedFeaturesIntoOptions ( userFeaturesToArray ( output , config ) , output ) ;
579
579
if ( ! userFeatures ) {
580
580
return { features : { } } ;
581
581
}
@@ -585,7 +585,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co
585
585
const features : Record < string , any > = { } ;
586
586
587
587
await Promise . all ( userFeatures . map ( async userFeature => {
588
- const userFeatureId = userFeature . userFeatureId ;
588
+ const userFeatureId = userFeature . rawUserFeatureId ;
589
589
const updatedFeatureId = getBackwardCompatibleFeatureId ( output , userFeatureId ) ;
590
590
const featureRef = getRef ( output , updatedFeatureId ) ;
591
591
if ( featureRef ) {
@@ -644,7 +644,12 @@ async function prepareOCICache(dstFolder: string) {
644
644
return ociCacheDir ;
645
645
}
646
646
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 {
648
653
if ( ! Object . keys ( config . features || { } ) . length && ! Object . keys ( additionalFeatures || { } ) . length ) {
649
654
return undefined ;
650
655
}
@@ -653,26 +658,28 @@ export function normalizedUserFeaturesToArray(config: DevContainerConfig, additi
653
658
const keys = new Set < string > ( ) ;
654
659
655
660
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 ] ;
659
664
const feature : DevContainerFeature = {
660
- userFeatureId : userFeatureKeyNormalized ,
665
+ rawUserFeatureId,
666
+ normalizedUserFeatureId,
661
667
options : userFeatureValue
662
668
} ;
663
669
userFeatures . push ( feature ) ;
664
- keys . add ( userFeatureKeyNormalized ) ;
670
+ keys . add ( normalizedUserFeatureId ) ;
665
671
}
666
672
}
667
673
668
674
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 ) ;
671
677
// 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 ] ;
674
680
const feature : DevContainerFeature = {
675
- userFeatureId : userFeatureKeyNormalized ,
681
+ rawUserFeatureId,
682
+ normalizedUserFeatureId,
676
683
options : userFeatureValue
677
684
} ;
678
685
userFeatures . push ( feature ) ;
@@ -712,11 +719,11 @@ export function updateDeprecatedFeaturesIntoOptions(userFeatures: DevContainerFe
712
719
713
720
const newFeaturePath = 'ghcr.io/devcontainers/features' ;
714
721
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 ) ;
718
725
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 } :` ) ) ;
720
727
if ( userFeature ) {
721
728
userFeature . options = {
722
729
...(
@@ -727,14 +734,16 @@ export function updateDeprecatedFeaturesIntoOptions(userFeatures: DevContainerFe
727
734
...withOptions ,
728
735
} ;
729
736
} else {
737
+ const rawUserFeatureId = `${ qualifiedMapToId } :${ versionBackwardComp } ` ;
730
738
userFeature = {
731
- userFeatureId : `${ qualifiedMapToId } :${ versionBackwardComp } ` ,
739
+ rawUserFeatureId,
740
+ normalizedUserFeatureId : normalizeUserFeatureIdentifier ( output , rawUserFeatureId ) ,
732
741
options : withOptions
733
742
} ;
734
743
userFeatures . push ( userFeature ) ;
735
744
}
736
745
}
737
- const updatedUserFeatures = userFeatures . filter ( feature => ! deprecatedFeaturesIntoOptions [ feature . userFeatureId ] ) ;
746
+ const updatedUserFeatures = userFeatures . filter ( feature => ! deprecatedFeaturesIntoOptions [ feature . rawUserFeatureId ] ) ;
738
747
return updatedUserFeatures ;
739
748
}
740
749
@@ -821,33 +830,26 @@ export function getBackwardCompatibleFeatureId(output: Log, id: string) {
821
830
export async function processFeatureIdentifier ( params : CommonParams , configPath : string | undefined , _workspaceRoot : string , userFeature : DevContainerFeature , lockfile ?: Lockfile ) : Promise < FeatureSet | undefined > {
822
831
const { output } = params ;
823
832
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 } ` ) ;
832
834
833
- const { type, manifest } = await getFeatureIdType ( params , userFeature . userFeatureId , lockfile ) ;
835
+ const { type, manifest } = await getFeatureIdType ( params , userFeature . normalizedUserFeatureId , lockfile ) ;
834
836
835
837
// cached feature
836
838
// Resolves deprecated features (fish, maven, gradle, homebrew, jupyterlab)
837
839
if ( type === 'local-cache' ) {
838
840
output . write ( `Cached feature found.` ) ;
839
841
840
842
let feat : Feature = {
841
- id : userFeature . userFeatureId ,
842
- name : userFeature . userFeatureId ,
843
+ id : userFeature . normalizedUserFeatureId ,
844
+ name : userFeature . normalizedUserFeatureId ,
843
845
value : userFeature . options ,
844
846
included : true ,
845
847
} ;
846
848
847
849
let newFeaturesSet : FeatureSet = {
848
850
sourceInformation : {
849
851
type : 'local-cache' ,
850
- userFeatureId : originalUserFeatureId
852
+ userFeatureId : userFeature . rawUserFeatureId // Without backwards-compatible normalization
851
853
} ,
852
854
features : [ feat ] ,
853
855
} ;
@@ -858,7 +860,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
858
860
// remote tar file
859
861
if ( type === 'direct-tarball' ) {
860
862
output . write ( `Remote tar file found.` ) ;
861
- const tarballUri = new URL . URL ( userFeature . userFeatureId ) ;
863
+ const tarballUri = new URL . URL ( userFeature . rawUserFeatureId ) ;
862
864
863
865
const fullPath = tarballUri . pathname ;
864
866
const tarballName = fullPath . substring ( fullPath . lastIndexOf ( '/' ) + 1 ) ;
@@ -879,8 +881,8 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
879
881
}
880
882
881
883
let feat : Feature = {
882
- id : id ,
883
- name : userFeature . userFeatureId ,
884
+ id,
885
+ name : userFeature . normalizedUserFeatureId ,
884
886
value : userFeature . options ,
885
887
included : true ,
886
888
} ;
@@ -889,7 +891,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
889
891
sourceInformation : {
890
892
type : 'direct-tarball' ,
891
893
tarballUri : tarballUri . toString ( ) ,
892
- userFeatureId : originalUserFeatureId
894
+ userFeatureId : userFeature . normalizedUserFeatureId ,
893
895
} ,
894
896
features : [ feat ] ,
895
897
} ;
@@ -901,10 +903,8 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
901
903
if ( type === 'file-path' ) {
902
904
output . write ( `Local disk feature.` ) ;
903
905
904
- const id = path . basename ( userFeature . userFeatureId ) ;
905
-
906
906
// Fail on Absolute paths.
907
- if ( path . isAbsolute ( userFeature . userFeatureId ) ) {
907
+ if ( path . isAbsolute ( userFeature . normalizedUserFeatureId ) ) {
908
908
output . write ( 'An Absolute path to a local feature is not allowed.' , LogLevel . Error ) ;
909
909
return undefined ;
910
910
}
@@ -914,7 +914,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
914
914
output . write ( 'A local feature requires a configuration path.' , LogLevel . Error ) ;
915
915
return undefined ;
916
916
}
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
918
918
919
919
// Ensure we aren't escaping .devcontainer folder
920
920
const parent = path . join ( _workspaceRoot , '.devcontainer' ) ;
@@ -926,14 +926,13 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
926
926
return undefined ;
927
927
}
928
928
929
- output . write ( `Resolved: ${ userFeature . userFeatureId } -> ${ featureFolderPath } ` , LogLevel . Trace ) ;
929
+ output . write ( `Resolved: ${ userFeature . rawUserFeatureId } -> ${ featureFolderPath } ` , LogLevel . Trace ) ;
930
930
931
931
// -- All parsing and validation steps complete at this point.
932
932
933
- output . write ( `Parsed feature id: ${ id } ` , LogLevel . Trace ) ;
934
933
let feat : Feature = {
935
- id,
936
- name : userFeature . userFeatureId ,
934
+ id : path . basename ( userFeature . normalizedUserFeatureId ) ,
935
+ name : userFeature . normalizedUserFeatureId ,
937
936
value : userFeature . options ,
938
937
included : true ,
939
938
} ;
@@ -942,7 +941,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
942
941
sourceInformation : {
943
942
type : 'file-path' ,
944
943
resolvedFilePath : featureFolderPath ,
945
- userFeatureId : originalUserFeatureId
944
+ userFeatureId : userFeature . normalizedUserFeatureId
946
945
} ,
947
946
features : [ feat ] ,
948
947
} ;
@@ -952,19 +951,19 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
952
951
953
952
// (6) Oci Identifier
954
953
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 ) ;
956
955
}
957
956
958
957
output . write ( `Github feature.` ) ;
959
958
// Github repository source.
960
959
let version = 'latest' ;
961
- let splitOnAt = userFeature . userFeatureId . split ( '@' ) ;
960
+ let splitOnAt = userFeature . normalizedUserFeatureId . split ( '@' ) ;
962
961
if ( splitOnAt . length > 2 ) {
963
962
output . write ( `Parse error. Use the '@' symbol only to designate a version tag.` , LogLevel . Error ) ;
964
963
return undefined ;
965
964
}
966
965
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 ) ;
968
967
version = splitOnAt [ 1 ] ;
969
968
}
970
969
@@ -975,7 +974,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
975
974
// eg: <publisher>/<feature-set>/<feature>
976
975
if ( splitOnSlash . length !== 3 || splitOnSlash . some ( x => x === '' ) || ! allowedFeatureIdRegex . test ( splitOnSlash [ 2 ] ) ) {
977
976
// 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 ) ;
979
978
return undefined ;
980
979
}
981
980
const owner = splitOnSlash [ 0 ] ;
@@ -984,12 +983,12 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
984
983
985
984
let feat : Feature = {
986
985
id : id ,
987
- name : userFeature . userFeatureId ,
986
+ name : userFeature . normalizedUserFeatureId ,
988
987
value : userFeature . options ,
989
988
included : true ,
990
989
} ;
991
990
992
- const userFeatureIdWithoutVersion = originalUserFeatureId . split ( '@' ) [ 0 ] ;
991
+ const userFeatureIdWithoutVersion = userFeature . normalizedUserFeatureId . split ( '@' ) [ 0 ] ;
993
992
if ( version === 'latest' ) {
994
993
let newFeaturesSet : FeatureSet = {
995
994
sourceInformation : {
@@ -999,7 +998,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
999
998
owner,
1000
999
repo,
1001
1000
isLatest : true ,
1002
- userFeatureId : originalUserFeatureId ,
1001
+ userFeatureId : userFeature . normalizedUserFeatureId ,
1003
1002
userFeatureIdWithoutVersion
1004
1003
} ,
1005
1004
features : [ feat ] ,
@@ -1016,7 +1015,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath:
1016
1015
repo,
1017
1016
tag : version ,
1018
1017
isLatest : false ,
1019
- userFeatureId : originalUserFeatureId ,
1018
+ userFeatureId : userFeature . normalizedUserFeatureId ,
1020
1019
userFeatureIdWithoutVersion
1021
1020
} ,
1022
1021
features : [ feat ] ,
0 commit comments