From 6bb79ef033c2a2e8f12c9da6409e3547af40417c Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 18 Oct 2018 07:05:43 +0200 Subject: Use ranges for dependency version --- CMakeLists.txt | 2 + README.md | 22 ++- backend/BackendUtils.cpp | 1 + include/Conf.hpp | 4 +- include/Dependency.hpp | 3 +- include/FileUtil.hpp | 5 +- include/GlobalLib.hpp | 4 +- include/Package.hpp | 8 +- include/PkgConfig.hpp | 2 + include/Version.hpp | 68 ++++++++ include/VersionParser.hpp | 51 ++++++ project.conf | 2 +- src/CmakeModule.cpp | 1 + src/Conf.cpp | 23 ++- src/FileUtil.cpp | 27 ++- src/GlobalLib.cpp | 40 ++--- src/Package.cpp | 65 +++++--- src/PkgConfig.cpp | 48 +++++- src/Version.cpp | 86 ++++++++++ src/VersionParser.cpp | 362 ++++++++++++++++++++++++++++++++++++++++ src/main.cpp | 2 +- tests/src/confTest/confTest.cpp | 4 +- tests/src/versionTest.cpp | 112 +++++++++++++ 23 files changed, 860 insertions(+), 82 deletions(-) create mode 100644 include/Version.hpp create mode 100644 include/VersionParser.hpp create mode 100644 src/Version.cpp create mode 100644 src/VersionParser.cpp create mode 100644 tests/src/versionTest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 285f69d..1febc5e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,8 @@ set(SOURCE_FILES src/Package.cpp src/GitRepository.cpp src/Platform.cpp + src/Version.cpp + src/VersionParser.cpp depends/libninja/src/Ninja.cpp) diff --git a/README.md b/README.md index d2159b9..2166be0 100644 --- a/README.md +++ b/README.md @@ -118,7 +118,7 @@ Required ### type Required. Should be one of: "executable", "static", "dynamic", "library" ### version -Required. Version string has to be in the format of "xxx.yyy.zzz" where xxx is major, yyy is minor and zzz is patch +Required. Version string has to be in the format of "xxx.yyy.zzz" where xxx is major, yyy is minor and zzz is patch. Version format is based on [semver 2.0.0](https://semver.org/spec/v2.0.0.html) ### platforms Required. A list of platforms the package supports. Can contain the following values: "any", "posix", "posix32", "posix64", linux", "linux32", "linux64", "win", "win32", "win64", "macos32", "macos64", "bsd", "openbsd", "openbsd32", "openbsd64", "haiku", "haiku32", "haiku64". If platforms contains "any" then there is no need to specify other platforms @@ -126,8 +126,24 @@ If platforms contains "any" then there is no need to specify other platforms Optional. A list of authors ## dependencies Optional. A list of dependencies which are specified in name-value pairs where the name is the name of the dependency, which should match the dependency name under the packages name specified in its project.conf file. -Currently, the value is the version and has to be an exact match for the package version, which is specified in the dependencies project.conf file. -This will later change and you should be able to choose minimum version and range of versions. +The value should be a version string, which specified the range of versions that you want to accept as a dependency to only allow dependency version that has the features you need and the version which hasn't changed its interface. +These are examples of the version string format: +``` +# Version 1.0.0 or above and less than 2.0.0, same as >=1.0.0 and <2.0.0 +1.0.0 +# Version 1.0.0 or above +>=1.0.0 +# Version above 1.0.0 +>1.0.0 +# Version exactly 1.0.0 +=1.0.0 +# Version less than 1.0.0 +<1.0.0 +# Version 1.0 or above but less than 2.0 +1.0 and <2.0 +# Version above 1.0 but less or equal to 1.3.2 +>1 and <=1.3.2 +``` Dependencies are automatically choosen from system (linux, mac) or if no package manager exists, then it's download from an url (see https://gitlab.com/DEC05EBA/sibs_packages). The dependency can also be a git project, in which case it will have the fields 'git' and optionally 'branch' and 'revision'. 'git' specifies the url to the git repository, 'branch' is the git branch that should be used - defaults to 'master'. diff --git a/backend/BackendUtils.cpp b/backend/BackendUtils.cpp index b724f51..37a5002 100644 --- a/backend/BackendUtils.cpp +++ b/backend/BackendUtils.cpp @@ -122,6 +122,7 @@ namespace backend collectSourceFiles(file->path, ninjaProject, sibsConfig, true); } } + return true; }); } } diff --git a/include/Conf.hpp b/include/Conf.hpp index a60bc36..ee61a21 100644 --- a/include/Conf.hpp +++ b/include/Conf.hpp @@ -8,6 +8,7 @@ #include "Dependency.hpp" #include "Package.hpp" #include "Platform.hpp" +#include "Version.hpp" #include #include #include @@ -402,7 +403,8 @@ namespace sibs bool zigTestAllFiles; bool packaging; bool bundling; - std::string version; + std::string versionStr; + PackageVersion version; Platform platform; protected: virtual void processObject(StringView name) override; diff --git a/include/Dependency.hpp b/include/Dependency.hpp index 7c8bbf1..0381625 100644 --- a/include/Dependency.hpp +++ b/include/Dependency.hpp @@ -3,6 +3,7 @@ #include #include +#include "Version.hpp"; namespace sibs { @@ -45,7 +46,7 @@ namespace sibs } std::string name; - std::string version; + PackageVersionRange version; }; class GitDependency : public Dependency diff --git a/include/FileUtil.hpp b/include/FileUtil.hpp index 3e2e302..0487227 100644 --- a/include/FileUtil.hpp +++ b/include/FileUtil.hpp @@ -36,7 +36,8 @@ namespace sibs void replaceChar(FileString &input, wchar_t charToReplace, wchar_t charToReplaceWith); #endif - using FileWalkCallbackFunc = std::function; + // Return true if you want to continue iterating the remaining files, return false if you want to stop + using FileWalkCallbackFunc = std::function; enum class FileType { @@ -48,7 +49,7 @@ namespace sibs FileType getFileType(const _tinydir_char_t *path); void walkDir(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc); void walkDirFiles(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc); - void walkDirFilesRecursive(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc); + bool walkDirFilesRecursive(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc); Result getFileContent(const _tinydir_char_t *filepath); Result fileWrite(const _tinydir_char_t *filepath, StringView data); Result fileOverwrite(const _tinydir_char_t *filepath, StringView data); diff --git a/include/GlobalLib.hpp b/include/GlobalLib.hpp index 8647a65..6a6c72a 100644 --- a/include/GlobalLib.hpp +++ b/include/GlobalLib.hpp @@ -21,9 +21,9 @@ namespace sibs static Result getLibs(const std::vector &libs, const SibsConfig &parentConfig, const FileString &globalLibRootDir, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallbackFunc, GlobalIncludeDirCallbackFunc globalIncludeDirCallback); static Result validatePackageExists(const FileString &globalLibRootDir, const std::string &name); - static Result getLibsLinkerFlags(const SibsConfig &parentConfig, const FileString &globalLibRootDir, const std::string &name, const std::string &version, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallbackFunc, GlobalIncludeDirCallbackFunc globalIncludeDirCallback); + static Result getLibsLinkerFlags(const SibsConfig &parentConfig, const FileString &globalLibRootDir, const std::string &name, const PackageVersionRange &versionRange, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallbackFunc, GlobalIncludeDirCallbackFunc globalIncludeDirCallback); static Result getLibsLinkerFlags(const SibsConfig &parentConfig, const FileString &globalLibRootDir, GitDependency *gitDependency, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallbackFunc, GlobalIncludeDirCallbackFunc globalIncludeDirCallback); - static Result downloadDependency(PackageListDependency *dependency); + static Result downloadDependency(PackageListDependency *dependency, Platform platform); static Result downloadDependency(GitDependency *dependency); private: static Result getLibsLinkerFlagsCommon(const SibsConfig &parentConfig, const FileString &packageDir, const std::string &dependencyName, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallbackFunc, GlobalIncludeDirCallbackFunc globalIncludeDirCallback); diff --git a/include/Package.hpp b/include/Package.hpp index dbae5f2..72652be 100644 --- a/include/Package.hpp +++ b/include/Package.hpp @@ -4,6 +4,7 @@ #include "../external/rapidjson/document.h" #include "Platform.hpp" #include "Result.hpp" +#include "Version.hpp" #include #include @@ -30,7 +31,7 @@ namespace sibs struct PackageMetadata { std::string description; - std::string version; + PackageVersion version; std::vector platforms; std::vector urls; }; @@ -45,11 +46,10 @@ namespace sibs static Result getPackageList(const char *url); /* - * Return the first url in list. + * Return the package data for the package we can use * TODO: If we fail to fetch package from first url, try other other ones in the list (or if the first url is too slow / takes too long to respond). - * TODO: Add version matching with wildcard etc. If we specify "1.2.*", then it should get the latest version that matches; etc... */ - static Result getPackageUrl(const char *packageName, const char *packageVersion, Platform platform); + static Result getPackage(const char *packageName, const PackageVersionRange &versionRange, Platform platform); }; } diff --git a/include/PkgConfig.hpp b/include/PkgConfig.hpp index 9181a2f..24aeb78 100644 --- a/include/PkgConfig.hpp +++ b/include/PkgConfig.hpp @@ -6,6 +6,7 @@ #include #include #include "FileUtil.hpp" +#include "Version.hpp" namespace sibs { @@ -24,6 +25,7 @@ namespace sibs static Result validatePkgConfigPackageVersionExists(PackageListDependency *dependency); static Result validatePackageExists(const std::string &name); static Result validatePackageVersionAtLeast(const std::string &name, const std::string &version); + static Result getPackageVersion(const std::string &name); static Result getDynamicLibsLinkerFlags(const std::vector &libs); static Result getDynamicLibsCflags(const std::vector &libs); static Result getDynamicLibsFlags(const std::vector &libs); diff --git a/include/Version.hpp b/include/Version.hpp new file mode 100644 index 0000000..915f622 --- /dev/null +++ b/include/Version.hpp @@ -0,0 +1,68 @@ +#pragma once + +#include + +namespace sibs +{ + enum class VersionOperation + { + LESS, + LESS_EQUAL, + EQUAL, + GREATER, + GREATER_EQUAL + }; + + const char* asString(VersionOperation operation); + + struct PackageVersion + { + int major; + int minor; + int patch; + + bool operator < (const PackageVersion &other) const + { + if(major < other.major) return true; + if(major == other.major && minor < other.minor) return true; + if(major == other.major && minor == other.minor && patch < other.patch) return true; + return false; + } + + bool operator == (const PackageVersion &other) const + { + return (major == other.major) && (minor == other.minor) && (patch == other.patch); + } + + bool operator <= (const PackageVersion &other) const + { + return *this < other || *this == other; + } + + std::string toString() const; + }; + static_assert(sizeof(PackageVersion) == sizeof(int) * 3, "Expected PackageVersion to be the same size as 3 ints"); + + struct PackageVersionRange + { + PackageVersionRange() + { + start = { 0, 0, 0 }; + end = { 0, 0, 0 }; + startDefined = false; + endDefined = false; + startOperation = VersionOperation::LESS; + endOperation = VersionOperation::LESS; + } + + bool isInRange(const PackageVersion &version) const; + std::string toString() const; + + PackageVersion start; + PackageVersion end; + bool startDefined; + bool endDefined; + VersionOperation startOperation; + VersionOperation endOperation; + }; +} \ No newline at end of file diff --git a/include/VersionParser.hpp b/include/VersionParser.hpp new file mode 100644 index 0000000..eba4d33 --- /dev/null +++ b/include/VersionParser.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include "types.hpp" +#include "Version.hpp" +#include "Result.hpp" +#include "StringView.hpp" +#include + +namespace sibs +{ + Result parsePackageVersion(const StringView &versionStr, int *size); + + enum class VersionToken + { + NONE, + END_OF_FILE, + INVALID, + OPERATION, + AND, + VERSION_NUMBER + }; + + struct VersionTokenizer + { + VersionTokenizer(); + VersionTokenizer(const char *start, const usize size); + VersionTokenizer(const VersionTokenizer &other); + VersionToken next(); + + + const char *start; + const char *code; + usize size; + usize index; + PackageVersion version; + VersionOperation operation; + StringView identifier; + std::string errMsg; + }; + + struct VersionParser + { + Result parse(const char *code, const usize size); + + VersionTokenizer tokenizer; + PackageVersionRange versionRange; + private: + VersionToken parseStart(); + VersionToken parseEnd(); + }; +} \ No newline at end of file diff --git a/project.conf b/project.conf index f042daf..5d4e5cb 100644 --- a/project.conf +++ b/project.conf @@ -1,7 +1,7 @@ [package] name = "sibs" type = "executable" -version = "0.1.5" +version = "0.2.0" authors = ["DEC05EBA <0xdec05eba@gmail.com>"] platforms = ["any"] diff --git a/src/CmakeModule.cpp b/src/CmakeModule.cpp index 1e873cd..91c572d 100644 --- a/src/CmakeModule.cpp +++ b/src/CmakeModule.cpp @@ -239,6 +239,7 @@ namespace sibs } } } + return true; }); // TODO: Clean this up. The below code is indentical to code in Ninja.cpp....... diff --git a/src/Conf.cpp b/src/Conf.cpp index a204f13..7ae589e 100644 --- a/src/Conf.cpp +++ b/src/Conf.cpp @@ -1,5 +1,6 @@ #include "../include/Conf.hpp" #include "../include/types.hpp" +#include "../include/VersionParser.hpp" #include "../external/utf8/unchecked.h" #include @@ -482,7 +483,7 @@ namespace sibs return Result::Err(errMsg); } - if(config.version.empty()) + if(config.versionStr.empty()) { string errMsg = "The project "; errMsg += config.getPackageName(); @@ -670,6 +671,7 @@ namespace sibs { if(_tinydir_strcmp(file->extension, CONFIG_STATIC_LIB_FILE_EXTENSION) == 0) outputFiles.push_back(toUtf8(file->path)); + return true; }); } @@ -747,12 +749,17 @@ namespace sibs else if(name.equals("version")) { if (value.isSingle()) - version = string(value.asSingle().data, value.asSingle().size); + versionStr = string(value.asSingle().data, value.asSingle().size); else throw ParserException("Expected package.version to be a single value, was a list"); - if(!isVersionStringValid(version)) - throw ParserException("package.version is in invalid format. Version string can only contain numbers and dots"); + int versionSize = 0; + Result versionResult = parsePackageVersion(value.asSingle(), &versionSize); + if(!versionResult) + throw ParserException("package.version is in invalid format, error: " + versionResult.getErrMsg()); + if(versionSize != (int)versionStr.size()) + throw ParserException("package.version is in invalid format, expected to only contain numbers and dots"); + version = versionResult.unwrap(); } else if(name.equals("authors")) { @@ -1025,10 +1032,14 @@ namespace sibs { if(value.isSingle()) { - // TODO: Validate version is number in correct format + VersionParser versionParser; + Result dependencyVersionResult = versionParser.parse(value.asSingle().data, value.asSingle().size); + if(!dependencyVersionResult) + throw ParserException("Dependency " + string(name.data, name.size) + " version is in invalid format, error: " + dependencyVersionResult.getErrMsg()); + PackageListDependency *dependency = new PackageListDependency(); dependency->name = string(name.data, name.size); - dependency->version = string(value.asSingle().data, value.asSingle().size); + dependency->version = dependencyVersionResult.unwrap(); packageListDependencies.emplace_back(dependency); } else if(value.isObject()) diff --git a/src/FileUtil.cpp b/src/FileUtil.cpp index b41808b..d33c94d 100644 --- a/src/FileUtil.cpp +++ b/src/FileUtil.cpp @@ -175,7 +175,11 @@ namespace sibs tinydir_file file; tinydir_readfile(&dir, &file); if(_tinydir_strncmp(file.name, TINYDIR_STRING("."), 1) != 0) - callbackFunc(&file); + { + bool doContinue = callbackFunc(&file); + if(!doContinue) + break; + } tinydir_next(&dir); } @@ -193,7 +197,11 @@ namespace sibs tinydir_file file; tinydir_readfile(&dir, &file); if(file.is_reg) - callbackFunc(&file); + { + bool doContinue = callbackFunc(&file); + if(!doContinue) + break; + } tinydir_next(&dir); } @@ -201,7 +209,7 @@ namespace sibs } // TODO: Handle failure (directory doesn't exist, no permission etc) - void walkDirFilesRecursive(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc) + bool walkDirFilesRecursive(const _tinydir_char_t *directory, FileWalkCallbackFunc callbackFunc) { tinydir_dir dir; tinydir_open(&dir, directory); @@ -211,13 +219,22 @@ namespace sibs tinydir_file file; tinydir_readfile(&dir, &file); if(file.is_reg) - callbackFunc(&file); + { + bool doContinue = callbackFunc(&file); + if(!doContinue) + return false; + } else if(_tinydir_strncmp(file.name, TINYDIR_STRING("."), 1) != 0) - walkDirFilesRecursive(file.path, callbackFunc); + { + bool doContinue = walkDirFilesRecursive(file.path, callbackFunc); + if(!doContinue) + return false; + } tinydir_next(&dir); } tinydir_close(&dir); + return true; } Result getFileContent(const _tinydir_char_t *filepath) diff --git a/src/GlobalLib.cpp b/src/GlobalLib.cpp index 242e621..61e6648 100644 --- a/src/GlobalLib.cpp +++ b/src/GlobalLib.cpp @@ -8,6 +8,7 @@ #include "../include/CmakeModule.hpp" #include "../include/Dependency.hpp" #include "../include/GitRepository.hpp" +#include "../include/VersionParser.hpp" using namespace std; @@ -69,7 +70,7 @@ namespace sibs // TODO: If return error is invalid url, then the message should be converted to // invalid package name/version. A check should be done if it is the name or version // that is invalid. - Result downloadDependencyResult = GlobalLib::downloadDependency(globalLibDependency); + Result downloadDependencyResult = GlobalLib::downloadDependency(globalLibDependency, parentConfig.platform); if(!downloadDependencyResult) return downloadDependencyResult; @@ -118,7 +119,7 @@ namespace sibs return Result::Ok(true); } - Result GlobalLib::getLibsLinkerFlags(const SibsConfig &parentConfig, const FileString &globalLibRootDir, const std::string &name, const std::string &version, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallbackFunc, GlobalIncludeDirCallbackFunc globalIncludeDirCallback) + Result GlobalLib::getLibsLinkerFlags(const SibsConfig &parentConfig, const FileString &globalLibRootDir, const std::string &name, const PackageVersionRange &versionRange, LinkerFlagCallbackFunc staticLinkerFlagCallbackFunc, LinkerFlagCallbackFunc dynamicLinkerFlagCallbackFunc, GlobalIncludeDirCallbackFunc globalIncludeDirCallback) { Result packageExistsResult = validatePackageExists(globalLibRootDir, name); if (packageExistsResult.isErr()) @@ -126,33 +127,34 @@ namespace sibs #if OS_FAMILY == OS_FAMILY_POSIX FileString namePlatformNative = name; - FileString versionPlatformNative = version; #else FileString namePlatformNative = utf8To16(name); - FileString versionPlatformNative = utf8To16(version); #endif FileString packageDir = globalLibRootDir + TINYDIR_STRING("/"); packageDir += namePlatformNative; - // TODO: Instead of checking if version is exact match, check if package has same major version - // and same or newer minor version FileString foundVersion; - walkDir(packageDir.c_str(), [&foundVersion, &versionPlatformNative](tinydir_file *file) + walkDir(packageDir.c_str(), [&foundVersion, &versionRange](tinydir_file *file) { if(file->is_dir) { - //printf("version: %s\n", file->name); - if(_tinydir_strcmp(versionPlatformNative.c_str(), file->name) == 0) + string versionUtf8 = toUtf8(file->name); + Result versionResult = parsePackageVersion({ versionUtf8.data(), versionUtf8.size() }, nullptr); + if(versionResult && versionRange.isInRange(versionResult.unwrap())) + { foundVersion = file->name; + return false; + } } + return true; }); if(foundVersion.empty()) - return Result::Err("Global lib dependency found, but version doesn't match dependency version", DependencyError::DEPENDENCY_VERSION_NO_MATCH); + return Result::Err("Global lib dependency found, but version isn't in range of version", DependencyError::DEPENDENCY_VERSION_NO_MATCH); packageDir += TINYDIR_STRING("/"); - packageDir += versionPlatformNative; + packageDir += foundVersion; return GlobalLib::getLibsLinkerFlagsCommon(parentConfig, packageDir, name, staticLinkerFlagCallbackFunc, dynamicLinkerFlagCallbackFunc, globalIncludeDirCallback); } @@ -261,13 +263,13 @@ namespace sibs } } - Result GlobalLib::downloadDependency(PackageListDependency *dependency) + Result GlobalLib::downloadDependency(PackageListDependency *dependency, Platform platform) { - Result packageUrlResult = Package::getPackageUrl(dependency->name.c_str(), dependency->version.c_str(), SYSTEM_PLATFORM); - if(!packageUrlResult) - return Result::Err(packageUrlResult); + Result packageResult = Package::getPackage(dependency->name.c_str(), dependency->version, platform); + if(!packageResult) + return Result::Err(packageResult); - const string &url = packageUrlResult.unwrap(); + const PackageMetadata &package = packageResult.unwrap(); Result libPathResult = getHomeDir(); if (!libPathResult) @@ -276,7 +278,7 @@ namespace sibs libPath += TINYDIR_STRING("/.cache/sibs/lib/"); libPath += toFileString(dependency->name); libPath += TINYDIR_STRING("/"); - libPath += toFileString(dependency->version); + libPath += package.version.toString(); FileString libArchivedFilePath = libPathResult.unwrap(); libArchivedFilePath += TINYDIR_STRING("/.cache/sibs/archive/"); @@ -286,8 +288,8 @@ namespace sibs return createArchiveDirResult; libArchivedFilePath += TINYDIR_STRING("/"); - libArchivedFilePath += toFileString(dependency->version); - Result downloadResult = curl::downloadFile(url.c_str(), libArchivedFilePath.c_str()); + libArchivedFilePath += package.version.toString(); + Result downloadResult = curl::downloadFile(package.urls[0].c_str(), libArchivedFilePath.c_str()); if(!downloadResult) return downloadResult; diff --git a/src/Package.cpp b/src/Package.cpp index eab0680..925de82 100644 --- a/src/Package.cpp +++ b/src/Package.cpp @@ -1,5 +1,6 @@ #include "../include/Package.hpp" #include "../include/curl.hpp" +#include "../include/VersionParser.hpp" #include "../external/rapidjson/error/en.h" #ifdef GetObject @@ -37,7 +38,15 @@ namespace sibs PackageMetadata packageMetadata; packageMetadata.description.assign(description->value.GetString(), description->value.GetStringLength()); - packageMetadata.version.assign(version->value.GetString(), version->value.GetStringLength()); + + int versionStrSize = 0; + Result versionResult = parsePackageVersion({ version->value.GetString(), version->value.GetStringLength() }, &versionStrSize); + if(!versionResult) + return Result::Err("package version is in wrong fromat, error: " + versionResult.getErrMsg()); + if(versionStrSize != (int)version->value.GetStringLength()) + return Result::Err("package version is in wrong format, expected only numbers and dots (version: " + string(version->value.GetString(), version->value.GetStringLength()) + ")"); + + packageMetadata.version = versionResult.unwrap(); const auto &platformsArray = platforms->value.GetArray(); packageMetadata.platforms.reserve(platformsArray.Size()); @@ -50,6 +59,9 @@ namespace sibs } const auto &urlsArray = urls->value.GetArray(); + if(urlsArray.Empty()) + return Result::Err("Expected url list to not be empty"); + packageMetadata.urls.reserve(urlsArray.Size()); for(int i = 0; i < urlsArray.Size(); ++i) { @@ -62,16 +74,16 @@ namespace sibs return Result::Ok(packageMetadata); } - static Result getPackageUrl(const PackageMetadata &packageMetadata, const char *packageName, const char *packageVersion, Platform platform) + static Result isPackageUsableForPlatform(const PackageMetadata &packageMetadata, const char *packageName, const PackageVersionRange &versionRange, Platform platform) { - if(strcmp(packageMetadata.version.c_str(), packageVersion) != 0) + if(!versionRange.isInRange(packageMetadata.version)) { string errMsg = "Package \""; errMsg += packageName; - errMsg += "\" does not exist for version \""; - errMsg += packageVersion; + errMsg += "\" does not exist for version range \""; + errMsg += versionRange.toString(); errMsg += "\""; - return Result::Err(errMsg); + return Result::Err(errMsg); } if(!containsPlatform(getPlatformsByNames(packageMetadata.platforms), platform)) @@ -79,14 +91,14 @@ namespace sibs string errMsg = "Package \""; errMsg += packageName; errMsg += "\" with version \""; - errMsg += packageVersion; + errMsg += packageMetadata.version.toString(); errMsg += "\" does not support platform \""; errMsg += platform; errMsg += "\""; - return Result::Err(errMsg); + return Result::Err(errMsg); } - - return Result::Ok(packageMetadata.urls[0]); + + return Result::Ok(true); } // TODO: Always downloading is fine right now because the package list is small. This should later be modified to use local cache. @@ -120,11 +132,11 @@ namespace sibs return Result::Ok(packageList); } - Result Package::getPackageUrl(const char *packageName, const char *packageVersion, Platform platform) + Result Package::getPackage(const char *packageName, const PackageVersionRange &versionRange, Platform platform) { Result packageList = Package::getPackageList("https://gitlab.com/DEC05EBA/sibs_packages/raw/master/packages.json"); if(!packageList) - return Result::Err(packageList); + return Result::Err(packageList); const Document &packageDoc = *packageList.unwrap(); @@ -134,14 +146,18 @@ namespace sibs string errMsg = "No package with the name \""; errMsg += packageName; errMsg += "\" was found"; - return Result::Err(errMsg); + return Result::Err(errMsg); } if(packageMetaDataJsonIt->value.IsObject()) { Result packageMetadataResult = getPackageMetadata(packageMetaDataJsonIt->value.GetObject()); - if(!packageMetadataResult) return Result::Err(packageMetadataResult); - return ::sibs::getPackageUrl(packageMetadataResult.unwrap(), packageName, packageVersion, platform); + if(!packageMetadataResult) return packageMetadataResult; + Result packageUsableResult = isPackageUsableForPlatform(packageMetadataResult.unwrap(), packageName, versionRange, platform); + if(packageUsableResult) + return Result::Ok(packageMetadataResult.unwrap()); + else + return Result::Err(packageUsableResult); } else if(packageMetaDataJsonIt->value.IsArray()) { @@ -155,35 +171,32 @@ namespace sibs errMsg += "["; errMsg += to_string(i); errMsg += "] is not an object"; - return Result::Err(errMsg); + return Result::Err(errMsg); } Result packageMetadataResult = getPackageMetadata(packageData.GetObject()); - if(packageMetadataResult) - { - Result packageUrlResult = ::sibs::getPackageUrl(packageMetadataResult.unwrap(), packageName, packageVersion, platform); - if(packageUrlResult) - return packageUrlResult; - } + if(!packageMetadataResult) return packageMetadataResult; + if(isPackageUsableForPlatform(packageMetadataResult.unwrap(), packageName, versionRange, platform)) + return Result::Ok(packageMetadataResult.unwrap()); ++i; } string errMsg = "Package \""; errMsg += packageName; - errMsg += "\" with version \""; - errMsg += packageVersion; + errMsg += "\" in version range \""; + errMsg += versionRange.toString(); errMsg += "\" does not exist or does not exist for platform \""; errMsg += asString(platform); errMsg += "\""; - return Result::Err(errMsg); + return Result::Err(errMsg); } else { string errMsg = "No package with the name \""; errMsg += packageName; errMsg += "\" was found"; - return Result::Err(errMsg); + return Result::Err(errMsg); } } } diff --git a/src/PkgConfig.cpp b/src/PkgConfig.cpp index e8f742c..89d3a44 100644 --- a/src/PkgConfig.cpp +++ b/src/PkgConfig.cpp @@ -1,6 +1,7 @@ #include "../include/PkgConfig.hpp" #include "../include/Exec.hpp" #include "../include/Dependency.hpp" +#include "../include/VersionParser.hpp" using namespace std; @@ -32,9 +33,12 @@ namespace sibs if(dependencyValidationResult.isErr()) return Result::Err(dependencyValidationResult.getErrMsg()); - Result dependencyVersionValidationResult = PkgConfig::validatePackageVersionAtLeast(dependency->name, dependency->version); - if(dependencyVersionValidationResult.isErr()) - return Result::Err(dependencyVersionValidationResult.getErrMsg()); + Result dependencyVersionResult = PkgConfig::getPackageVersion(dependency->name); + if(!dependencyVersionResult) + return Result::Err(dependencyVersionResult); + + if(!dependency->version.isInRange(dependencyVersionResult.unwrap())) + return Result::Err("pkg-config package " + dependency->name + " exists but the version does not match our expected version range"); return Result::Ok(true); } @@ -70,11 +74,6 @@ namespace sibs Result PkgConfig::validatePackageVersionAtLeast(const string &name, const string &version) { - // TODO: Instead of checking if package version is same or newer, check if package is same major version - // and same or newer minor version - - // Use --modversion instead and check if the version returned is newer or equal to dependency version. - // This way we can output installed version vs expected dependency version FileString command = pkgConfigPath + TINYDIR_STRING(" '--atleast-version="); command += toFileString(version); command += TINYDIR_STRING("' '"); @@ -98,7 +97,7 @@ namespace sibs } else if(execResult.unwrap().exitCode != 0) { - string errMsg = "Failed to check dependency version, Unknown error, exit code: "; + string errMsg = "Failed to check pkg-config package version, Unknown error, exit code: "; errMsg += to_string(execResult.unwrap().exitCode); return Result::Err(errMsg); } @@ -106,6 +105,37 @@ namespace sibs return Result::Ok(true); } + Result PkgConfig::getPackageVersion(const std::string &name) + { + FileString command = pkgConfigPath + TINYDIR_STRING(" --modversion '"); + command += toFileString(name); + command += TINYDIR_STRING("'"); + Result execResult = exec(command.c_str()); + if(!execResult) + return Result::Err(execResult.getErrMsg()); + + if(execResult.unwrap().exitCode == 1) + { + string errMsg = "Dependency "; + errMsg += name; + errMsg += " not found in pkg-config"; + return Result::Err(errMsg); + } + else if(execResult.unwrap().exitCode == 127) + { + return Result::Err("pkg-config is not installed"); + } + else if(execResult.unwrap().exitCode != 0) + { + string errMsg = "Failed to get pkg-config package version, Unknown error, exit code: "; + errMsg += to_string(execResult.unwrap().exitCode); + return Result::Err(errMsg); + } + + // Intentionally allow packages which have a version that contains more data after patch number, since some pkg-config packages are not in semver format + return parsePackageVersion({ execResult.unwrap().execStdout.data(), execResult.unwrap().execStdout.size() }, nullptr); + } + Result PkgConfig::getDynamicLibsLinkerFlags(const vector &libs) { if(libs.empty()) return Result::Ok(""); diff --git a/src/Version.cpp b/src/Version.cpp new file mode 100644 index 0000000..26e5dbd --- /dev/null +++ b/src/Version.cpp @@ -0,0 +1,86 @@ +#include "../include/Version.hpp" +#include + +namespace sibs +{ + const char* asString(VersionOperation operation) + { + switch(operation) + { + case VersionOperation::LESS: return "<"; + case VersionOperation::LESS_EQUAL: return "<="; + case VersionOperation::EQUAL: return "="; + case VersionOperation::GREATER: return ">"; + case VersionOperation::GREATER_EQUAL: return ">="; + default: return ""; + } + } + + std::string PackageVersion::toString() const + { + std::string result; + result += std::to_string(major); + result += "."; + result += std::to_string(minor); + result += "."; + result += std::to_string(patch); + return result; + } + + std::string PackageVersionRange::toString() const + { + std::string result; + result += asString(startOperation); + result += start.toString(); + if(endDefined) + { + result += " and "; + result += asString(endOperation); + result += end.toString(); + } + return result; + } + + static bool isInRangeOfEnd(const PackageVersionRange &versionRange, const PackageVersion &version) + { + if(!versionRange.endDefined) + return true; + + switch(versionRange.endOperation) + { + case VersionOperation::LESS: + return version < versionRange.end; + case VersionOperation::LESS_EQUAL: + return version <= versionRange.end; + default: + assert(false); + return true; + } + } + + bool PackageVersionRange::isInRange(const PackageVersion &version) const + { + switch(startOperation) + { + case VersionOperation::LESS: + return version < start; + case VersionOperation::LESS_EQUAL: + return version <= start; + case VersionOperation::EQUAL: + return version == start; + case VersionOperation::GREATER: + { + if(version <= start) + return false; + return isInRangeOfEnd(*this, version); + } + case VersionOperation::GREATER_EQUAL: + { + if(version < start) + return false; + return isInRangeOfEnd(*this, version); + } + } + return true; + } +} \ No newline at end of file diff --git a/src/VersionParser.cpp b/src/VersionParser.cpp new file mode 100644 index 0000000..7b28cef --- /dev/null +++ b/src/VersionParser.cpp @@ -0,0 +1,362 @@ +#include "../include/VersionParser.hpp" +#include "../include/StringView.hpp" + +namespace sibs +{ + static int stringToIntNoVerify(const StringView &str) + { + int result = 0; + if(str.size > 0) + result += (str[0] - '0'); + + for(int i = 1; i < (int)str.size; ++i) + { + int num = str[i] - '0'; + result += (10 * ((int)str.size - i) * num); + } + return result; + } + + static bool isNum(char c) + { + return c >= '0' && c <= '9'; + } + + static bool isAlpha(char c) + { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); + } + + Result parsePackageVersion(const StringView &versionStr, int *size) + { + PackageVersion result = { 0, 0, 0 }; + int *versionPtr = &result.major; + int digitStart = 0; + int i = 0; + + for(; i < (int)versionStr.size; ++i) + { + char c = versionStr[i]; + if(isNum(c)) + { + if(digitStart == -1) + digitStart = i; + } + else + { + int length = i - digitStart; + if(digitStart == -1 || length == 0) + return Result::Err("Package version string is in invalid format. Expected to be in format xxx.xxx.xxx where x is a number"); + + *versionPtr = stringToIntNoVerify({ versionStr.data + digitStart, (usize)(i - digitStart) }); + bool endOfVersions = versionPtr == &result.patch; + ++versionPtr; + digitStart = -1; + + if(c != '.' || endOfVersions) + break; + } + } + + if(i == 0) + return Result::Err("version can't be empty"); + + if(digitStart != -1) + { + *versionPtr = stringToIntNoVerify({ versionStr.data + digitStart, (usize)(i - digitStart) }); + ++versionPtr; + } + + if(size) + *size = i; + + if(versionPtr == &result.major) + return Result::Err("version can't be empty"); + + return Result::Ok(result); + } + + VersionTokenizer::VersionTokenizer() : + start(nullptr), + code(nullptr), + size(0), + index(0) + { + + } + + VersionTokenizer::VersionTokenizer(const char *_start, const usize _size) : + start(_start), + code(_start), + size(_size), + index(0) + { + + } + + VersionTokenizer::VersionTokenizer(const VersionTokenizer &other) + { + start = other.start; + code = other.code; + size = other.size; + index = other.index; + } + + VersionToken VersionTokenizer::next() + { + while(index < size) + { + char c = code[index]; + if(c == ' ' || c == '\t' || c == '\n' || c == '\r') + ++index; + else + break; + } + + if(index >= size) + return VersionToken::END_OF_FILE; + + char c = code[index]; + if(isNum(c)) + { + int versionStrSize = 0; + identifier.data = code + index; + Result packageVersion = parsePackageVersion({ code + index, (usize)(size - index) }, &versionStrSize); + identifier.size = versionStrSize; + index += versionStrSize; + if(!packageVersion) + { + errMsg = packageVersion.getErrMsg(); + return VersionToken::INVALID; + } + version = packageVersion.unwrap(); + return VersionToken::VERSION_NUMBER; + } + else if(isAlpha(c)) + { + usize identifierStart = index; + ++index; + while(index < size && isAlpha(code[index])) + { + ++index; + } + usize identifierEnd = index; + usize identifierLength = identifierEnd - identifierStart; + + if(identifierLength == 3 && strncmp(code + identifierStart, "and", 3) == 0) + { + return VersionToken::AND; + } + else + { + errMsg = "Invalid identifier "; + errMsg += std::string(code + identifierStart, identifierLength); + return VersionToken::INVALID; + } + } + else if(c == '<') + { + ++index; + if(index < size && code[index] == '=') + { + ++index; + operation = VersionOperation::LESS_EQUAL; + return VersionToken::OPERATION; + } + operation = VersionOperation::LESS; + return VersionToken::OPERATION; + } + else if(c == '=') + { + ++index; + operation = VersionOperation::EQUAL; + return VersionToken::OPERATION; + } + else if(c == '>') + { + ++index; + if(index < size && code[index] == '=') + { + ++index; + operation = VersionOperation::GREATER_EQUAL; + return VersionToken::OPERATION; + } + operation = VersionOperation::GREATER; + return VersionToken::OPERATION; + } + else + { + errMsg = "Unexpected character "; + errMsg += c; + return VersionToken::INVALID; + } + } + + Result VersionParser::parse(const char *code, const usize size) + { + versionRange = PackageVersionRange(); + tokenizer = VersionTokenizer(code, size); + VersionToken token = parseStart(); + + if(token == VersionToken::END_OF_FILE) + { + if(!versionRange.startDefined) + return Result::Err("version can't be empty"); + return Result::Ok(versionRange); + } + else if(token == VersionToken::INVALID) + return Result::Err(tokenizer.errMsg); + else + { + std::string errMsg = "Unexpected token '"; + switch(token) + { + case VersionToken::NONE: + { + errMsg += ""; + break; + } + case VersionToken::OPERATION: + { + errMsg += "operation "; + errMsg += asString(tokenizer.operation); + break; + } + case VersionToken::AND: + { + errMsg += "and"; + break; + } + case VersionToken::VERSION_NUMBER: + { + errMsg += "version "; + errMsg += std::string(tokenizer.identifier.data, tokenizer.identifier.size); + break; + } + default: + break; + } + errMsg += "'"; + return Result::Err(errMsg); + } + } + + VersionToken VersionParser::parseStart() + { + VersionToken token = tokenizer.next(); + if(token == VersionToken::VERSION_NUMBER) + { + versionRange.startOperation = VersionOperation::GREATER_EQUAL; + versionRange.start = tokenizer.version; + versionRange.startDefined = true; + token = tokenizer.next(); + if(token == VersionToken::AND) + { + token = VersionToken::INVALID; + tokenizer.errMsg = "Unexpected end version when start version does not have operation defined"; + } + } + else if(token == VersionToken::OPERATION) + { + versionRange.startOperation = tokenizer.operation; + token = tokenizer.next(); + if(token == VersionToken::VERSION_NUMBER) + { + versionRange.start = tokenizer.version; + versionRange.startDefined = true; + switch(versionRange.startOperation) + { + case VersionOperation::LESS: + { + token = VersionToken::INVALID; + tokenizer.errMsg = "Unexpected version end when start version is expected to less than "; + tokenizer.errMsg += versionRange.start.toString(); + return token; + } + case VersionOperation::LESS_EQUAL: + { + token = VersionToken::INVALID; + tokenizer.errMsg = "Unexpected version end when start version is expected to be less or equal to "; + tokenizer.errMsg += versionRange.start.toString(); + return token; + } + case VersionOperation::EQUAL: + { + token = VersionToken::INVALID; + tokenizer.errMsg = "Unexpected version end when start version is expected to be exactly "; + tokenizer.errMsg += versionRange.start.toString(); + return token; + } + default: + break; + } + + token = tokenizer.next(); + if(token == VersionToken::AND) + { + return parseEnd(); + } + } + else if(token == VersionToken::INVALID) + return token; + else + { + token = VersionToken::INVALID; + tokenizer.errMsg = "Expected version after operation"; + } + } + return token; + } + + VersionToken VersionParser::parseEnd() + { + VersionToken token = tokenizer.next(); + if(token == VersionToken::OPERATION) + { + versionRange.endOperation = tokenizer.operation; + switch(versionRange.endOperation) + { + case VersionOperation::EQUAL: + case VersionOperation::GREATER: + case VersionOperation::GREATER_EQUAL: + { + token = VersionToken::INVALID; + tokenizer.errMsg = "End version can only have operations < and <="; + return token; + } + default: + break; + } + + token = tokenizer.next(); + if(token == VersionToken::VERSION_NUMBER) + { + versionRange.end = tokenizer.version; + versionRange.endDefined = true; + if(versionRange.end <= versionRange.start) + { + token = VersionToken::INVALID; + tokenizer.errMsg = "Expected version end to be greater than "; + tokenizer.errMsg += versionRange.start.toString(); + tokenizer.errMsg += ", was "; + tokenizer.errMsg += versionRange.end.toString(); + return token; + } + token = tokenizer.next(); + } + else if(token == VersionToken::INVALID) + return token; + else + { + token = VersionToken::INVALID; + tokenizer.errMsg = "Expected version after operation"; + } + } + else + { + token = VersionToken::INVALID; + tokenizer.errMsg = "Expected end version to have operation defined"; + } + return token; + } +} diff --git a/src/main.cpp b/src/main.cpp index 4bf840a..5d43679 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1173,7 +1173,7 @@ static int packageProject(int argc, const _tinydir_char_t **argv) TINYDIR_STRING("\" \"") + executablePath + TINYDIR_STRING("\" \"") + - toFileString(sibsConfig.version) + + toFileString(sibsConfig.version.toString()) + TINYDIR_STRING("\" \"") + packagePath + TINYDIR_STRING("\" ") + diff --git a/tests/src/confTest/confTest.cpp b/tests/src/confTest/confTest.cpp index 8dab691..a86dbb3 100644 --- a/tests/src/confTest/confTest.cpp +++ b/tests/src/confTest/confTest.cpp @@ -26,11 +26,11 @@ TEST_CASE("parse config") } auto *xxhashDependency = sibsConfig.getPackageListDependencies()[0]->asPackageListDependency(); REQUIRE(xxhashDependency->name == "xxhash"); - REQUIRE(xxhashDependency->version == "0.1.0"); + REQUIRE(xxhashDependency->version.toString() == ">=0.1.0"); const auto &catch2Dependency = sibsConfig.getPackageListDependencies()[1]->asPackageListDependency(); REQUIRE(catch2Dependency->name == "catch2"); - REQUIRE(catch2Dependency->version == "1.0.0"); + REQUIRE(catch2Dependency->version.toString() == ">=1.0.0"); REQUIRE(sibsConfig.getGitDependencies().size() == 1); for(auto *dep : sibsConfig.getGitDependencies()) diff --git a/tests/src/versionTest.cpp b/tests/src/versionTest.cpp new file mode 100644 index 0000000..d5a492c --- /dev/null +++ b/tests/src/versionTest.cpp @@ -0,0 +1,112 @@ +#include +#include "../../include/VersionParser.hpp" + +using namespace sibs; + +TEST_CASE("parse package version") +{ + VersionParser parser; + { + Result parseResult = parser.parse("2.3.4", 5); + if(!parseResult) + FAIL(parseResult.getErrMsg()); + PackageVersionRange versionRange = parseResult.unwrap(); + REQUIRE(versionRange.startDefined); + REQUIRE(!versionRange.endDefined); + REQUIRE(versionRange.startOperation == VersionOperation::GREATER_EQUAL); + REQUIRE(versionRange.start.major == 2); + REQUIRE(versionRange.start.minor == 3); + REQUIRE(versionRange.start.patch == 4); + } + { + Result parseResult = parser.parse("2.3", 3); + if(!parseResult) + FAIL(parseResult.getErrMsg()); + PackageVersionRange versionRange = parseResult.unwrap(); + REQUIRE(versionRange.startDefined); + REQUIRE(!versionRange.endDefined); + REQUIRE(versionRange.startOperation == VersionOperation::GREATER_EQUAL); + REQUIRE(versionRange.start.major == 2); + REQUIRE(versionRange.start.minor == 3); + REQUIRE(versionRange.start.patch == 0); + } + { + Result parseResult = parser.parse("2", 1); + if(!parseResult) + FAIL(parseResult.getErrMsg()); + PackageVersionRange versionRange = parseResult.unwrap(); + REQUIRE(versionRange.startDefined); + REQUIRE(!versionRange.endDefined); + REQUIRE(versionRange.startOperation == VersionOperation::GREATER_EQUAL); + REQUIRE(versionRange.start.major == 2); + REQUIRE(versionRange.start.minor == 0); + REQUIRE(versionRange.start.patch == 0); + } + { + Result parseResult = parser.parse(">=2.3.4 and <5.6.7", 18); + if(!parseResult) + FAIL(parseResult.getErrMsg()); + PackageVersionRange versionRange = parseResult.unwrap(); + REQUIRE(versionRange.startDefined); + REQUIRE(versionRange.endDefined); + REQUIRE(versionRange.startOperation == VersionOperation::GREATER_EQUAL); + REQUIRE(versionRange.start.major == 2); + REQUIRE(versionRange.start.minor == 3); + REQUIRE(versionRange.start.patch == 4); + REQUIRE(versionRange.endOperation == VersionOperation::LESS); + REQUIRE(versionRange.end.major == 5); + REQUIRE(versionRange.end.minor == 6); + REQUIRE(versionRange.end.patch == 7); + } + { + Result parseResult = parser.parse(">2.3.4 and <=5.6.7", 18); + if(!parseResult) + FAIL(parseResult.getErrMsg()); + PackageVersionRange versionRange = parseResult.unwrap(); + REQUIRE(versionRange.startDefined); + REQUIRE(versionRange.endDefined); + REQUIRE(versionRange.startOperation == VersionOperation::GREATER); + REQUIRE(versionRange.start.major == 2); + REQUIRE(versionRange.start.minor == 3); + REQUIRE(versionRange.start.patch == 4); + REQUIRE(versionRange.endOperation == VersionOperation::LESS_EQUAL); + REQUIRE(versionRange.end.major == 5); + REQUIRE(versionRange.end.minor == 6); + REQUIRE(versionRange.end.patch == 7); + } + { + Result parseResult = parser.parse(">2.3.4 and <=5", 14); + if(!parseResult) + FAIL(parseResult.getErrMsg()); + PackageVersionRange versionRange = parseResult.unwrap(); + REQUIRE(versionRange.startDefined); + REQUIRE(versionRange.endDefined); + REQUIRE(versionRange.startOperation == VersionOperation::GREATER); + REQUIRE(versionRange.start.major == 2); + REQUIRE(versionRange.start.minor == 3); + REQUIRE(versionRange.start.patch == 4); + REQUIRE(versionRange.endOperation == VersionOperation::LESS_EQUAL); + REQUIRE(versionRange.end.major == 5); + REQUIRE(versionRange.end.minor == 0); + REQUIRE(versionRange.end.patch == 0); + } + { + REQUIRE(!parser.parse("", 0)); + REQUIRE(!parser.parse(" ", 1)); + REQUIRE(!parser.parse("2", 2)); + REQUIRE(!parser.parse(".", 1)); + REQUIRE(!parser.parse(".2", 2)); + REQUIRE(!parser.parse("<", 1)); + REQUIRE(!parser.parse("and", 3)); + REQUIRE(!parser.parse("2 and", 5)); + REQUIRE(!parser.parse("2 and 3", 7)); + REQUIRE(!parser.parse("2 and =3", 8)); + REQUIRE(!parser.parse("2 and >3", 8)); + REQUIRE(!parser.parse(">=1.0.0 and 2.0.0", 17)); + REQUIRE(!parser.parse("1.0.0 < 2.0.0", 13)); + REQUIRE(!parser.parse("1.0.0 and <0.9.3", 16)); + REQUIRE(!parser.parse("=1.0.0 and <2.0.0", 17)); + REQUIRE(!parser.parse("1.0.0 and <2.0.0", 16)); + REQUIRE(!parser.parse(">=1.0.0 and =2.0.0", 18)); + } +} -- cgit v1.2.3