diff --git a/src/haxelib/Data.hx b/src/haxelib/Data.hx index e2548bc01..5dfe930b9 100644 --- a/src/haxelib/Data.hx +++ b/src/haxelib/Data.hx @@ -298,6 +298,18 @@ abstract ProjectName(String) to String { case v: v; } + /** + If `alias` is just a different capitalization of `correct`, returns `correct`. + + If `alias` is completely different, returns `alias` instead. + **/ + static public function getCorrectOrAlias(correct:ProjectName, alias:ProjectName) { + return if (correct.toLowerCase() == alias.toLowerCase()) + correct + else + alias; + } + /** Default project name **/ static public var DEFAULT(default, null) = new ProjectName('unknown'); } @@ -420,15 +432,22 @@ class Data { } } - /** Extracts project information from `jsondata`, validating it according to `check`. **/ - public static function readData( jsondata: String, check : CheckLevel ) : Infos { + /** + Extracts project information from `jsondata`, validating it according to `check`. + + `defaultName` is the project name to use if it is empty when the check value allows it. + **/ + public static function readData( jsondata: String, check : CheckLevel, ?defaultName:ProjectName ) : Infos { + if (defaultName == null) + defaultName = ProjectName.DEFAULT; + var doc:Infos = try Json.parse(jsondata) catch ( e : Dynamic ) if (check >= CheckLevel.CheckSyntax) throw 'JSON parse error: $e'; else { - name : ProjectName.DEFAULT, + name : defaultName, url : '', version : SemVer.DEFAULT, releasenote: 'No haxelib.json found', @@ -457,7 +476,7 @@ class Data { doc.classPath = ''; if (doc.name.validate() != None) - doc.name = ProjectName.DEFAULT; + doc.name = defaultName; if (doc.description == null) doc.description = ''; diff --git a/src/haxelib/api/GlobalScope.hx b/src/haxelib/api/GlobalScope.hx index 63893ca56..ee04161a2 100644 --- a/src/haxelib/api/GlobalScope.hx +++ b/src/haxelib/api/GlobalScope.hx @@ -37,7 +37,8 @@ class GlobalScope extends Scope { Dependency.fromNameAndVersion(name, version)]; final libraryRunData = { - name: library, + name: ProjectName.getCorrectOrAlias(info.name, library), + internalName: info.name, version: resolved.version, dependencies: dependencies, path: resolved.path, @@ -139,7 +140,9 @@ class GlobalScope extends Scope { while (!stack.isEmpty()) { final cur = stack.pop(); - final library = cur.library; + // turn it to lowercase always (so that `LiBrArY:1.2.0` and `library:1.3.0` clash because + // they are different versions), and then get the correct name if provided + final library = repository.getCorrectName(ProjectName.ofString(cur.library.toLowerCase())); final version = cur.version; // check for duplicates @@ -147,7 +150,7 @@ class GlobalScope extends Scope { final otherVersion = includedLibraries[library]; // if the current library is part of the original set of inputs, and if the versions don't match if (topLevelLibs.contains(cur) && version != null && version != otherVersion) - throw 'Cannot process `$library:$version`:' + throw 'Cannot process `${cur.library}:$version`: ' + 'Library $library has two versions included : $otherVersion and $version'; continue; } @@ -167,7 +170,7 @@ class GlobalScope extends Scope { final info = { final jsonContent = try File.getContent(resolved.path + Data.JSON) catch (_) null; - Data.readData(jsonContent, jsonContent != null ? CheckSyntax : NoCheck); + Data.readData(jsonContent, jsonContent != null ? CheckSyntax : NoCheck, library); } // path and version compiler define @@ -177,7 +180,9 @@ class GlobalScope extends Scope { else resolved.path ); - addLine('-D $library=${info.version}'); + // if info.name is not the placeholder for an empty name (i.e. unknown), use it, otherwise + // fall back to the value in the .name file + addLine('-D ${info.name}=${info.version}'); // add dependencies to stack final dependencies = info.dependencies.toArray(); diff --git a/src/haxelib/api/Installer.hx b/src/haxelib/api/Installer.hx index 327ec6f9b..4dd014937 100644 --- a/src/haxelib/api/Installer.hx +++ b/src/haxelib/api/Installer.hx @@ -275,20 +275,24 @@ class Installer { continue; } - if (library.isLatest || !scope.isLibraryInstalled(library.name)) - setVersionAndLog(library.name, library.installData); + final libraryName = switch library.installData { + case VcsInstall(version, vcsData): getName(library.name, version, vcsData.subDir); + case _: library.name; + } + + if (library.isLatest || !scope.isLibraryInstalled(libraryName)) + setVersionAndLog(libraryName, library.installData); userInterface.log("Done"); - handleDependenciesGeneral(library.name, library.installData); + handleDependenciesGeneral(libraryName, library.installData); } } /** Installs the latest version of `library` from the haxelib server. **/ public function installLatestFromHaxelib(library:ProjectName) { final info = Connection.getInfo(library); - // TODO with stricter capitalisation we won't have to check this maybe - // that way we only have to check the versions + // get correct name capitalization so that the logged output is correct final library = ProjectName.ofString(info.name); final versions = [for (v in info.versions) v.name]; @@ -313,8 +317,8 @@ class Installer { a global scope, the new version is always set as the current one. **/ public function installFromHaxelib(library:ProjectName, version:SemVer, forceSet:Bool = false) { - // TODO with stricter capitalisation we won't have to check this maybe final info = Connection.getInfo(library); + // get correct name capitalization so that the logged output is correct final library = ProjectName.ofString(info.name); final versions = [for (v in info.versions) v.name]; @@ -344,6 +348,8 @@ class Installer { public function installVcsLibrary(library:ProjectName, id:VcsID, vcsData:VcsData) { installVcs(library, id, vcsData); + library = getName(library, id, vcsData.subDir); + scope.setVcsVersion(library, id, vcsData); if (vcsData.subDir != null) { @@ -452,6 +458,19 @@ class Installer { } } + /** Get the name found in the `haxelib.json` for a vcs library. + + If `givenName` is an alias (it is completely different from the internal name) + then `givenName` is returned instead + **/ + function getName(givenName:ProjectName, id:VcsID, subDir:Null):ProjectName { + final jsonPath = scope.getPath(givenName, id) + (if (subDir != null) subDir else "") + Data.JSON; + if (!FileSystem.exists(jsonPath)) + return givenName; + final internalName = Data.readData(File.getContent(jsonPath), false).name; + return ProjectName.getCorrectOrAlias(internalName, givenName); + } + function handleDependenciesGeneral(library:ProjectName, installData:InstallData) { if (skipDependencies) return; @@ -499,23 +518,28 @@ class Installer { final installData = getInstallData(libs); for (lib in installData) { - final library = lib.name; final version = lib.version; - userInterface.log('Installing dependency $library $version'); + userInterface.log('Installing dependency ${lib.name} $version'); switch lib.installData { - case Haxelib(v) if (!forceInstallDependencies && repository.isVersionInstalled(library, v)): - userInterface.log('Library $library version $v is already installed'); + case Haxelib(v) if (!forceInstallDependencies && repository.isVersionInstalled(lib.name, v)): + userInterface.log('Library ${lib.name} version $v is already installed'); continue; default: } try - installFromInstallData(library, lib.installData) + installFromInstallData(lib.name, lib.installData) catch (e) { userInterface.log(e.toString()); continue; } + + final library = switch lib.installData { + case VcsInstall(version, vcsData): getName(lib.name, version, vcsData.subDir); + case _: lib.name; + } + // vcs versions always get set if (!scope.isLibraryInstalled(library) || lib.installData.match(VcsInstall(_))) { setVersionAndLog(library, lib.installData); @@ -674,8 +698,7 @@ class Installer { function installZip(library:ProjectName, version:SemVer, zip:List):Void { userInterface.log('Installing $library...'); - final rootPath = repository.getProjectPath(library); - FsUtils.safeDir(rootPath); + final versionPath = repository.getVersionPath(library, version); FsUtils.safeDir(versionPath); @@ -780,6 +803,7 @@ class Installer { } } } else { + FsUtils.safeDir(libPath); doVcsClone(); } diff --git a/src/haxelib/api/Repository.hx b/src/haxelib/api/Repository.hx index 8993ad3b2..ff7edb903 100644 --- a/src/haxelib/api/Repository.hx +++ b/src/haxelib/api/Repository.hx @@ -21,8 +21,12 @@ class CurrentVersionException extends haxe.Exception {} directly modifying them. **/ class Repository { + /** Name of file used to keep track of current version. **/ static final CURRENT = ".current"; + /** Name of file used to keep track of capitalization. **/ + static final NAME = ".name"; + /** The path to the repository. @@ -77,11 +81,14 @@ class Repository { var libraryName:ProjectName; for (dir in FileSystem.readDirectory(path)) { - // hidden or not a folder - if (dir.startsWith(".") || !FileSystem.isDirectory(Path.join([path, dir]))) + // hidden, not a folder, or has upper case letters + if (dir.startsWith(".") || !FileSystem.isDirectory(Path.join([path, dir])) || dir.toLowerCase() != dir) continue; - libraryName = try ProjectName.ofString(Data.unsafe(dir)) catch (_:haxe.Exception) continue; + libraryName = getCorrectProjectName( + try ProjectName.ofString(Data.unsafe(dir)) + catch (_:haxe.Exception) continue + ); if (!isFilteredOut(libraryName)) projects.push(libraryName); @@ -190,6 +197,9 @@ class Repository { Set current version of project `name` to `version`. Throws an error if `name` or `version` of `name` is not installed. + + `name` may also be used as the official version of the library name + if installing a proper semver release **/ public function setCurrentVersion(name:ProjectName, version:Version):Void { if (!FileSystem.exists(getProjectRootPath(name))) @@ -199,8 +209,15 @@ class Repository { throw 'Library $name version $version is not installed'; final currentFilePath = getCurrentFilePath(name); + final isNewLibrary = !FileSystem.exists(currentFilePath); File.saveContent(currentFilePath, version); + + // if the library is being installed for the first time, or this is a proper version + // or it is a git/hg/dev version but there are no proper versions installed + // proper semver releases replace names given by git/hg/dev versions + if (isNewLibrary || SemVer.isValid(version) || !doesLibraryHaveOfficialVersion(name)) + setCapitalizationIfNeeded(name); } /** @@ -263,10 +280,59 @@ class Repository { return getProjectVersionPath(name, version); } + /** + Returns the correctly capitalized name for library `name`. + + `name` can be any possible capitalization variation of the library name. + **/ + public function getCorrectName(name:ProjectName):ProjectName { + return getCorrectProjectName(name); + } + + static inline function doesNameHaveCapitals(name:ProjectName):Bool { + return name != ProjectName.ofString(name.toLowerCase()); + } + + function setCapitalizationIfNeeded(name:ProjectName):Void { + // if it is not all lowercase then we save the actual capitalisation in the `.name` file + final filePath = getNameFilePath(name); + + if (doesNameHaveCapitals(name)) { + File.saveContent(filePath, name); + return; + } + + try + FileSystem.deleteFile(filePath) + catch (_) { + // file didn't exist so we're good + } + } + + // returns whether or not `name` has any versions installed which are not dev/git/hg + function doesLibraryHaveOfficialVersion(name:ProjectName):Bool { + final root = getProjectRootPath(name); + + for (sub in FileSystem.readDirectory(root)) { + // ignore .dev and .current files + if (sub.startsWith(".")) + continue; + + final version = Data.unsafe(sub); + if (SemVer.isValid(version)) + return true; + } + return false; + } + inline function getCurrentFilePath(name:ProjectName):String { return addToRepoPath(name, CURRENT); } + inline function getNameFilePath(name:ProjectName):String { + return addToRepoPath(name, NAME); + } + inline function getCurrentFileContent(name:ProjectName):String { return try File.getContent(getCurrentFilePath(name)).trim() @@ -274,6 +340,13 @@ class Repository { throw new CurrentVersionException('No current version set for library \'$name\''); } + inline function getCorrectProjectName(name:ProjectName):ProjectName { + return try + ProjectName.ofString(File.getContent(getNameFilePath(name)).trim()) + catch(e:haxe.Exception) + name; + } + inline function getProjectRootPath(name:ProjectName):String { return addToRepoPath(name).addTrailingSlash(); } @@ -309,8 +382,13 @@ class Repository { public function setDevPath(name:ProjectName, path:String) { final root = getProjectRootPath(name); - if (!FileSystem.exists(root)) - FileSystem.createDirectory(root); + final isNew = !FileSystem.exists(root); + + FileSystem.createDirectory(root); + + if (isNew || !doesLibraryHaveOfficialVersion(name)) { + setCapitalizationIfNeeded(name); + } final devFile = Path.join([root, DEV]); diff --git a/src/haxelib/api/ScriptRunner.hx b/src/haxelib/api/ScriptRunner.hx index bffff64c3..c867367a0 100644 --- a/src/haxelib/api/ScriptRunner.hx +++ b/src/haxelib/api/ScriptRunner.hx @@ -37,7 +37,10 @@ typedef Dependencies = Array; @:noDoc /** Library data needed in order to run it **/ typedef LibraryRunData = { + /** This may be an alias. **/ name:ProjectName, + /** This is the actual name found in the `haxelib.json`. **/ + internalName:ProjectName, version:VersionOrDev, dependencies:Dependencies, main:String, @@ -90,7 +93,7 @@ class ScriptRunner { setState({ dir: library.path, run: "1", - runName: library.name + runName: library.internalName }); final output = Sys.command(cmd, args); diff --git a/src/haxelib/client/Main.hx b/src/haxelib/client/Main.hx index 901401445..07b6c7d94 100644 --- a/src/haxelib/client/Main.hx +++ b/src/haxelib/client/Main.hx @@ -437,10 +437,9 @@ class Main { return installer.installFromHaxelibJson(jsonPath); } } - // Name provided that wasn't a local hxml or zip, so try to install it from server - // TODO with stricter capitalisation we won't have to check this maybe final info = Connection.getInfo(ProjectName.ofString(toInstall)); + // for display purposes, here we use the corrected project name. final library = ProjectName.ofString(info.name); final versionGiven = argsIterator.next(); @@ -544,8 +543,6 @@ class Main { if (input == null) return installer.updateAll(); - /* TODO we used to check for all case combinations - by iterating through all library names */ final library = ProjectName.ofString(input); if (!scope.isLibraryInstalled(library)) { @@ -686,6 +683,16 @@ class Main { throw 'Directory $dir does not exist'; try { final dir = FileSystem.fullPath(dir); + + final project = { + final jsonPath = haxe.io.Path.join([dir, Data.JSON]); + if (!FileSystem.exists(jsonPath)) + project; + else { + final internalName = Data.readData(File.getContent(jsonPath), false).name; + ProjectName.getCorrectOrAlias(internalName, project); + } + } repository.setDevPath(project, dir); Cli.print('Development directory set to $dir'); } catch (e) {