From 36c5eab799c051cbe7dc4726819d74bd2b06899a Mon Sep 17 00:00:00 2001 From: pancake Date: Mon, 26 Oct 2020 18:05:18 +0100 Subject: [PATCH] all: add #pkgconfig directive using the new vlib modules (#6673) --- vlib/pkgconfig/README.md | 66 +++++++++ vlib/pkgconfig/bin/pkgconfig.v | 18 +++ vlib/pkgconfig/main.v | 209 +++++++++++++++++++++++++++ vlib/pkgconfig/pkgconfig.v | 250 +++++++++++++++++++++++++++++++++ vlib/pkgconfig/v.mod | 12 ++ vlib/semver/LICENSE.md | 21 +++ vlib/semver/README.md | 38 +++++ vlib/semver/compare.v | 61 ++++++++ vlib/semver/parse.v | 87 ++++++++++++ vlib/semver/range.v | 245 ++++++++++++++++++++++++++++++++ vlib/semver/semver.v | 82 +++++++++++ vlib/semver/semver_test.v | 192 +++++++++++++++++++++++++ vlib/semver/util.v | 57 ++++++++ vlib/semver/v.mod | 5 + vlib/v/checker/checker.v | 18 ++- 15 files changed, 1360 insertions(+), 1 deletion(-) create mode 100644 vlib/pkgconfig/README.md create mode 100644 vlib/pkgconfig/bin/pkgconfig.v create mode 100644 vlib/pkgconfig/main.v create mode 100644 vlib/pkgconfig/pkgconfig.v create mode 100644 vlib/pkgconfig/v.mod create mode 100644 vlib/semver/LICENSE.md create mode 100644 vlib/semver/README.md create mode 100644 vlib/semver/compare.v create mode 100644 vlib/semver/parse.v create mode 100644 vlib/semver/range.v create mode 100644 vlib/semver/semver.v create mode 100644 vlib/semver/semver_test.v create mode 100644 vlib/semver/util.v create mode 100644 vlib/semver/v.mod diff --git a/vlib/pkgconfig/README.md b/vlib/pkgconfig/README.md new file mode 100644 index 0000000000..9c4bec77e0 --- /dev/null +++ b/vlib/pkgconfig/README.md @@ -0,0 +1,66 @@ +v-pkgconfig +=========== + +This module implements the `pkg-config` tool as a library in pure V. + +Features: + +* Simple API, but still not stable, but shouldnt change much +* Runs 2x faster than original pkg-config +* Commandline tool that aims to be compatible with `pkg-config` +* Resolve full path for `.pc` file given a name +* Recursively parse all the dependencies +* Find and replace all inner variables + +Todo/Future/Wish: + +* 100% compatibility with `pkg-config` options +* Integration with V, to support pkgconfig with `system()` +* Strictier pc parsing logic, with better error reporting + +Example +------- + +The commandline tool is available in `vlib/pkgconfig/bin/pkgconfig.v` + +``` +$ ./bin/pkgconfig -h +pkgconfig 0.2.0 +----------------------------------------------- +Usage: pkgconfig [options] [ARGS] + +Options: + -V, --modversion show version of module + -d, --description show pkg module description + -h, --help show this help message + -D, --debug show debug information + -l, --list-all list all pkgmodules + -e, --exists return 0 if pkg exists + -V, --print-variables display variable names + -r, --print-requires display requires of the module + -a, --atleast-version + return 0 if pkg version is at least the given one + --exact-version + return 0 if pkg version is at least the given one + -v, --version show version of this tool + -c, --cflags output all pre-processor and compiler flags + -I, --cflags-only-I show only -I flags from CFLAGS + --cflags-only-other show cflags without -I + -s, --static show --libs for static linking + -l, --libs output all linker flags + --libs-only-l show only -l from ldflags + -L, --libs-only-L show only -L from ldflags + --libs-only-other show flags not containing -l or -L +$ +``` + +Using the API from the V repl + +``` +>>> import pkgconfig +>>> opt := pkgconfig.Options{} +>>> mut pc := pkgconfig.load('r_core', opt) or { panic(err) } +>>> pc.libs +['-L/usr/local/lib', '-lr_core', '-lr_config', '-lr_util', '', '-ldl', '-lr_cons', '-lr_io', '-lr_socket', '-lr_hash', '-lr_crypto', '-lr_flag', '-lr_asm', '-lr_syscall', '-lr_lang', '-lr_parse', '-lr_reg', '-lr_debug', '-lr_anal', '-lr_search', '-lr_bp', '-lr_egg', '-lr_bin', '-lr_magic', '-lr_fs'] +>>> +``` diff --git a/vlib/pkgconfig/bin/pkgconfig.v b/vlib/pkgconfig/bin/pkgconfig.v new file mode 100644 index 0000000000..3bd94768c9 --- /dev/null +++ b/vlib/pkgconfig/bin/pkgconfig.v @@ -0,0 +1,18 @@ +module main + +import pkgconfig +import os + +fn main() { + mut m := pkgconfig.main(os.args[1..]) or { + eprintln(err) + exit(1) + } + m.res = m.run() or { + eprintln(err) + exit(1) + } + if m.res != '' { + println(m.res) + } +} diff --git a/vlib/pkgconfig/main.v b/vlib/pkgconfig/main.v new file mode 100644 index 0000000000..6554baccb2 --- /dev/null +++ b/vlib/pkgconfig/main.v @@ -0,0 +1,209 @@ +module pkgconfig + +import flag +import strings + +pub struct Main { +pub mut: + opt &MainOptions + res string + has_actions bool +} + +struct MainOptions { + modversion bool + description bool + help bool + debug bool + listall bool + exists bool + variables bool + requires bool + atleast string + atleastpc string + exactversion string + version bool + cflags bool + cflags_only_path bool + cflags_only_other bool + stat1c bool + libs bool + libs_only_link bool + libs_only_path bool + libs_only_other bool + args []string +} + +fn desc(mod string) ?string { + options := Options{ + norecurse: true + } + mut pc := load(mod, options) or { + return error('cannot parse') + } + return pc.description +} + +pub fn main(args []string) ?&Main { + mut fp := flag.new_flag_parser(args) + fp.application('pkgconfig') + fp.version(version) + mut m := &Main{ + opt: parse_options(mut fp) + } + opt := m.opt + if opt.help { + m.res = fp.usage().replace('- ,', ' ') + } else if opt.version { + m.res = version + } else if opt.listall { + mut modules := list() + modules.sort() + if opt.description { + for mod in modules { + d := desc(mod) or { + continue + } + pad := strings.repeat(` `, 20 - mod.len) + m.res += '$mod $pad $d\n' + } + } else { + m.res = modules.join('\n') + } + } else if opt.args.len == 0 { + return error('No packages given') + } + return m +} + +pub fn (mut m Main) run() ?string { + options := Options{ + debug: m.opt.debug + } + // m.opt = options + opt := m.opt + mut pc := &PkgConfig(0) + mut res := m.res + for arg in opt.args { + mut pcdep := load(arg, options) or { + if !opt.exists { + return error(err) + } + continue + } + if opt.description { + if res != '' { + res += '\n' + } + res += pcdep.description + } + if pc != 0 { + pc.extend(pcdep) + } else { + pc = pcdep + } + } + if opt.exists { + return res + } + if opt.exactversion != '' { + if pc.version != opt.exactversion { + return error('version mismatch') + } + return res + } + if opt.atleast != '' { + if pc.atleast(opt.atleast) { + return error('version mismatch') + } + return res + } + if opt.atleastpc != '' { + if atleast(opt.atleastpc) { + return error('version mismatch') + } + return res + } + if opt.variables { + for k, _ in pc.vars { + res += '$k\n' + } + } + if opt.requires { + res += pc.requires.join('\n') + } + mut r := []string{} + if opt.cflags_only_path { + r << filter(pc.cflags, '-I', '') + } + if opt.cflags_only_other { + r << filter(pc.cflags, '-I', '-I') + } + if opt.cflags { + r << pc.cflags.join(' ') + } + if opt.libs_only_link { + r << filter(pc.libs, '-l', '') + } + if opt.libs_only_path { + r << filter(pc.libs, '-L', '') + } + if opt.libs_only_other { + r << filter(pc.libs, '-l', '-L') + } + if opt.libs { + if opt.stat1c { + r << pc.libs_private.join(' ') + } else { + r << pc.libs.join(' ') + } + } + if opt.modversion { + r << pc.version + } + return res + r.join(' ') +} + +fn filter(libs []string, prefix string, prefix2 string) string { + mut res := '' + if prefix2 != '' { + for lib in libs { + if !lib.starts_with(prefix) && !lib.starts_with(prefix2) { + res += ' $lib' + } + } + } else { + for lib in libs { + if lib.starts_with(prefix) { + res += ' $lib' + } + } + } + return res +} + +fn parse_options(mut fp flag.FlagParser) &MainOptions { + return &MainOptions{ + description: fp.bool('description', `d`, false, 'show pkg module description') + modversion: fp.bool('modversion', `V`, false, 'show version of module') + help: fp.bool('help', `h`, false, 'show this help message') + debug: fp.bool('debug', `D`, false, 'show debug information') + listall: fp.bool('list-all', `l`, false, 'list all pkgmodules') + exists: fp.bool('exists', `e`, false, 'return 0 if pkg exists') + variables: fp.bool('print-variables', `V`, false, 'display variable names') + requires: fp.bool('print-requires', `r`, false, 'display requires of the module') + atleast: fp.string('atleast-version', `a`, '', 'return 0 if pkg version is at least the given one') + atleastpc: fp.string('atleast-pkgconfig-version', `A`, '', 'return 0 if pkgconfig version is at least the given one') + exactversion: fp.string('exact-version', ` `, '', 'return 0 if pkg version is at least the given one') + version: fp.bool('version', `v`, false, 'show version of this tool') + cflags: fp.bool('cflags', `c`, false, 'output all pre-processor and compiler flags') + cflags_only_path: fp.bool('cflags-only-I', `I`, false, 'show only -I flags from CFLAGS') + cflags_only_other: fp.bool('cflags-only-other', ` `, false, 'show cflags without -I') + stat1c: fp.bool('static', `s`, false, 'show --libs for static linking') + libs: fp.bool('libs', `l`, false, 'output all linker flags') + libs_only_link: fp.bool('libs-only-l', ` `, false, 'show only -l from ldflags') + libs_only_path: fp.bool('libs-only-L', `L`, false, 'show only -L from ldflags') + libs_only_other: fp.bool('libs-only-other', ` `, false, 'show flags not containing -l or -L') + args: fp.args + } +} diff --git a/vlib/pkgconfig/pkgconfig.v b/vlib/pkgconfig/pkgconfig.v new file mode 100644 index 0000000000..2746aa54a1 --- /dev/null +++ b/vlib/pkgconfig/pkgconfig.v @@ -0,0 +1,250 @@ +module pkgconfig + +import semver +import os + +const ( + default_paths = [ + '/usr/local/lib/x86_64-linux-gnu/pkgconfig', + '/usr/local/lib/pkgconfig', + '/usr/local/share/pkgconfig', + '/usr/lib/x86_64-linux-gnu/pkgconfig', + '/usr/lib/pkgconfig', + '/usr/share/pkgconfig', + ] + version = '0.2.0' +) + +pub struct Options { +pub: + path string + debug bool + norecurse bool +} + +pub struct PkgConfig { +pub mut: + options Options + libs []string + libs_private []string + cflags []string + paths []string // TODO: move to options? + vars map[string]string + requires []string + version string + description string + name string + modname string +} + +fn (mut pc PkgConfig) filters(s string) []string { + r := pc.filter(s).split(' ') + mut res := []string{} + for a in r { + if a != '' { + res << a + } + } + return res +} + +fn (mut pc PkgConfig) filter(s string) string { + mut r := s.trim_space() + for r.contains('\${') { + tok0 := r.index('\${') or { + break + } + mut tok1 := r[tok0..].index('}') or { + break + } + tok1 += tok0 + v := r[tok0 + 2..tok1] + r = r.replace('\${$v}', pc.vars[v]) + } + return r.trim_space() +} + +fn (mut pc PkgConfig) setvar(line string) { + kv := line.trim_space().split('=') + if kv.len == 2 { + k := kv[0] + v := pc.filter(kv[1]) + pc.vars[k] = pc.filter(v) + } +} + +fn (mut pc PkgConfig) parse(file string) bool { + data := os.read_file(file) or { + return false + } + if pc.options.debug { + eprintln(data) + } + lines := data.split('\n') + if pc.options.norecurse { + // 2x faster than original pkg-config for --list-all --description + // TODO: use different variable. norecurse have nothing to do with this + for line in lines { + if line.starts_with('Description: ') { + pc.description = pc.filter(line[13..]) + } + } + } else { + for line in lines { + if line.starts_with('#') { + continue + } + if line.contains('=') && !line.contains(' ') { + pc.setvar(line) + continue + } + if line.starts_with('Description: ') { + pc.description = pc.filter(line[13..]) + } else if line.starts_with('Name: ') { + pc.name = pc.filter(line[6..]) + } else if line.starts_with('Version: ') { + pc.version = pc.filter(line[9..]) + } else if line.starts_with('Requires: ') { + pc.requires = pc.filters(line[10..]) + } else if line.starts_with('Cflags: ') { + pc.cflags = pc.filters(line[8..]) + } else if line.starts_with('Libs: ') { + pc.libs = pc.filters(line[6..]) + } else if line.starts_with('Libs.private: ') { + pc.libs_private = pc.filters(line[14..]) + } + } + } + return true +} + +fn (mut pc PkgConfig) resolve(pkgname string) ?string { + if pc.paths.len == 0 { + pc.paths << '.' + } + for path in pc.paths { + file := '$path/${pkgname}.pc' + if os.exists(file) { + return file + } + } + return error('Cannot find "$pkgname" pkgconfig file') +} + +pub fn atleast(v string) bool { + v0 := semver.from(version) or { + return false + } + v1 := semver.from(v) or { + return false + } + return v0.gt(v1) +} + +pub fn (mut pc PkgConfig) atleast(v string) bool { + v0 := semver.from(pc.version) or { + return false + } + v1 := semver.from(v) or { + return false + } + return v0.gt(v1) +} + +pub fn (mut pc PkgConfig) extend(pcdep &PkgConfig) ?string { + for flag in pcdep.cflags { + if pc.cflags.index(flag) == -1 { + pc.cflags << flag + } + } + for lib in pcdep.libs { + if pc.libs.index(lib) == -1 { + pc.libs << lib + } + } + for lib in pcdep.libs_private { + if pc.libs_private.index(lib) == -1 { + pc.libs_private << lib + } + } +} + +fn (mut pc PkgConfig) load_requires() { + for dep in pc.requires { + mut pcdep := PkgConfig{ + paths: pc.paths + } + depfile := pcdep.resolve(dep) or { + break + } + pcdep.parse(depfile) + pcdep.load_requires() + pc.extend(pcdep) + } +} + +fn (mut pc PkgConfig) add_path(path string) { + p := if path.ends_with('/') { path[0..path.len - 1] } else { path } + if pc.paths.index(p) == -1 { + pc.paths << p + } +} + +fn (mut pc PkgConfig) load_paths() { + for path in default_paths { + pc.add_path(path) + } + for path in pc.options.path.split(':') { + pc.add_path(path) + } + env_var := os.getenv('PKG_CONFIG_PATH') + if env_var != '' { + env_paths := env_var.trim_space().split(':') + for path in env_paths { + pc.add_path(path) + } + } +} + +pub fn load(pkgname string, options Options) ?&PkgConfig { + mut pc := &PkgConfig{ + modname: pkgname + options: options + } + pc.load_paths() + file := pc.resolve(pkgname) or { + return error(err) + } + pc.parse(file) + /* + if pc.name != pc.modname { + eprintln('Warning: modname and filename differ $pc.name $pc.modname') + } + */ + if !options.norecurse { + pc.load_requires() + } + return pc +} + +pub fn list() []string { + mut pc := &PkgConfig{ + options: Options{} + } + pc.load_paths() + mut modules := []string{} + for path in pc.paths { + files := os.ls(path) or { + continue + } + for file in files { + if file.ends_with('.pc') { + name := file.replace('.pc', '') + if modules.index(name) == -1 { + modules << name + } + } + } + } + return modules +} diff --git a/vlib/pkgconfig/v.mod b/vlib/pkgconfig/v.mod new file mode 100644 index 0000000000..9a23fc2cd5 --- /dev/null +++ b/vlib/pkgconfig/v.mod @@ -0,0 +1,12 @@ +Module { + name: 'pkgconfig' + author: 'pancake' + version: '0.2.0' + repo_url: 'https://github.com/trufae/v-pkgconfig' + vcs: 'git' + tags: ['system', 'compilers'] + dependencies: ['semver'] + description: 'V API implementing pkg-config logic' + license: 'MIT' +} + diff --git a/vlib/semver/LICENSE.md b/vlib/semver/LICENSE.md new file mode 100644 index 0000000000..8d5ff71e1f --- /dev/null +++ b/vlib/semver/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 alexesprit + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vlib/semver/README.md b/vlib/semver/README.md new file mode 100644 index 0000000000..bb38f4422a --- /dev/null +++ b/vlib/semver/README.md @@ -0,0 +1,38 @@ +# semver + +A library for working with versions in [semver][semver] format. + +## Usage + +```v +import semver + +fn main() { + ver1 := semver.from('1.2.4') or { + println('Invalid version') + return + } + ver2 := semver.from('2.3.4') or { + println('Invalid version') + return + } + + println(ver1.gt(ver2)) + println(ver2.gt(ver1)) + println(ver1.satisfies('>=1.1.0 <2.0.0')) + println(ver2.satisfies('>=1.1.0 <2.0.0')) + println(ver2.satisfies('>=1.1.0 <2.0.0 || >2.2.0')) +} +``` + +``` +false +true +true +false +true +``` + +For more details see `semver.v` file. + +[semver]: https://semver.org/ diff --git a/vlib/semver/compare.v b/vlib/semver/compare.v new file mode 100644 index 0000000000..07bf6fc9a5 --- /dev/null +++ b/vlib/semver/compare.v @@ -0,0 +1,61 @@ +module semver + +// * Private functions. +[inline] +fn version_satisfies(ver Version, input string) bool { + range := parse_range(input) or { + return false + } + return range.satisfies(ver) +} + +fn compare_eq(v1 Version, v2 Version) bool { + return v1.major == v2.major && + v1.minor == v2.minor && v1.patch == v2.patch && v1.prerelease == v2.prerelease +} + +fn compare_gt(v1 Version, v2 Version) bool { + if v1.major < v2.major { + return false + } + if v1.major > v2.major { + return true + } + if v1.minor < v2.minor { + return false + } + if v1.minor > v2.minor { + return true + } + return v1.patch > v2.patch +} + +fn compare_lt(v1 Version, v2 Version) bool { + if v1.major > v2.major { + return false + } + if v1.major < v2.major { + return true + } + if v1.minor > v2.minor { + return false + } + if v1.minor < v2.minor { + return true + } + return v1.patch < v2.patch +} + +fn compare_ge(v1 Version, v2 Version) bool { + if compare_eq(v1, v2) { + return true + } + return compare_gt(v1, v2) +} + +fn compare_le(v1 Version, v2 Version) bool { + if compare_eq(v1, v2) { + return true + } + return compare_lt(v1, v2) +} diff --git a/vlib/semver/parse.v b/vlib/semver/parse.v new file mode 100644 index 0000000000..292c0d50ca --- /dev/null +++ b/vlib/semver/parse.v @@ -0,0 +1,87 @@ +module semver + +// * Private structs and functions. +struct RawVersion { + prerelease string + metadata string +mut: + raw_ints []string +} + +const ( + ver_major = 0 + ver_minor = 1 + ver_patch = 2 + versions = [ver_major, ver_minor, ver_patch] +) + +// TODO: Rewrite using regexps? +// /(\d+)\.(\d+)\.(\d+)(?:\-([0-9A-Za-z-.]+))?(?:\+([0-9A-Za-z-]+))?/ +fn parse(input string) RawVersion { + mut raw_version := input + mut prerelease := '' + mut metadata := '' + plus_idx := raw_version.last_index('+') or { + -1 + } + if plus_idx > 0 { + metadata = raw_version[(plus_idx + 1)..] + raw_version = raw_version[0..plus_idx] + } + hyphen_idx := raw_version.index('-') or { + -1 + } + if hyphen_idx > 0 { + prerelease = raw_version[(hyphen_idx + 1)..] + raw_version = raw_version[0..hyphen_idx] + } + raw_ints := raw_version.split('.') + return RawVersion{ + prerelease: prerelease + metadata: metadata + raw_ints: raw_ints + } +} + +fn (ver RawVersion) is_valid() bool { + if ver.raw_ints.len != 3 { + return false + } + return is_valid_number(ver.raw_ints[ver_major]) && is_valid_number(ver.raw_ints[ver_minor]) && + is_valid_number(ver.raw_ints[ver_patch]) && is_valid_string(ver.prerelease) && is_valid_string(ver.metadata) +} + +fn (ver RawVersion) is_missing(typ int) bool { + return typ >= ver.raw_ints.len - 1 +} + +fn (raw_ver RawVersion) coerce() ?Version { + ver := raw_ver.complete() + if !is_valid_number(ver.raw_ints[ver_major]) { + return error('Invalid major version: $ver.raw_ints[ver_major]') + } + return ver.to_version() +} + +fn (raw_ver RawVersion) complete() RawVersion { + mut raw_ints := raw_ver.raw_ints + for raw_ints.len < 3 { + raw_ints << '0' + } + return RawVersion{ + prerelease: raw_ver.prerelease + metadata: raw_ver.metadata + raw_ints: raw_ints + } +} + +fn (raw_ver RawVersion) validate() ?Version { + if !raw_ver.is_valid() { + return none + } + return raw_ver.to_version() +} + +fn (raw_ver RawVersion) to_version() Version { + return Version{raw_ver.raw_ints[ver_major].int(), raw_ver.raw_ints[ver_minor].int(), raw_ver.raw_ints[ver_patch].int(), raw_ver.prerelease, raw_ver.metadata} +} diff --git a/vlib/semver/range.v b/vlib/semver/range.v new file mode 100644 index 0000000000..7e6c55810c --- /dev/null +++ b/vlib/semver/range.v @@ -0,0 +1,245 @@ +module semver + +// * Private functions. +const ( + comparator_sep = ' ' + comparator_set_sep = ' || ' + hyphen_range_sep = ' - ' + x_range_symbols = 'Xx*' +) + +enum Operator { + gt + lt + ge + le + eq +} + +struct Comparator { + ver Version + op Operator +} + +struct ComparatorSet { + comparators []Comparator +} + +struct Range { + comparator_sets []ComparatorSet +} + +fn (r Range) satisfies(ver Version) bool { + mut final_result := false + for set in r.comparator_sets { + final_result = final_result || set.satisfies(ver) + } + return final_result +} + +fn (set ComparatorSet) satisfies(ver Version) bool { + for comp in set.comparators { + if !comp.satisfies(ver) { + return false + } + } + return true +} + +fn (c Comparator) satisfies(ver Version) bool { + return match c.op { + .gt { ver.gt(c.ver) } + .lt { ver.lt(c.ver) } + .ge { ver.ge(c.ver) } + .le { ver.le(c.ver) } + .eq { ver.eq(c.ver) } + } +} + +fn parse_range(input string) ?Range { + raw_comparator_sets := input.split(comparator_set_sep) + mut comparator_sets := []ComparatorSet{} + for raw_comp_set in raw_comparator_sets { + if can_expand(raw_comp_set) { + s := expand_comparator_set(raw_comp_set) or { + return error(err) + } + comparator_sets << s + } else { + s := parse_comparator_set(raw_comp_set) or { + return error(err) + } + comparator_sets << s + } + } + return Range{comparator_sets} +} + +fn parse_comparator_set(input string) ?ComparatorSet { + raw_comparators := input.split(comparator_sep) + if raw_comparators.len > 2 { + return error('Invalid format of comparator set') + } + mut comparators := []Comparator{} + for raw_comp in raw_comparators { + c := parse_comparator(raw_comp) or { + return error('Invalid comparator: $raw_comp') + } + comparators << c + } + return ComparatorSet{comparators} +} + +fn parse_comparator(input string) ?Comparator { + mut op := Operator.eq + mut raw_version := '' + if input.starts_with('>=') { + op = .ge + raw_version = input[2..] + } else if input.starts_with('<=') { + op = .le + raw_version = input[2..] + } else if input.starts_with('>') { + op = .gt + raw_version = input[1..] + } else if input.starts_with('<') { + op = .lt + raw_version = input[1..] + } else if input.starts_with('=') { + raw_version = input[1..] + } else { + raw_version = input + } + version := coerce_version(raw_version) or { + return none + } + return Comparator{version, op} +} + +fn parse_xrange(input string) ?Version { + mut raw_ver := parse(input).complete() + for typ in versions { + if raw_ver.raw_ints[typ].index_any(x_range_symbols) == -1 { + continue + } + match typ { + ver_major { + raw_ver.raw_ints[ver_major] = '0' + raw_ver.raw_ints[ver_minor] = '0' + raw_ver.raw_ints[ver_patch] = '0' + } + ver_minor { + raw_ver.raw_ints[ver_minor] = '0' + raw_ver.raw_ints[ver_patch] = '0' + } + ver_patch { + raw_ver.raw_ints[ver_patch] = '0' + } + else {} + } + } + if !raw_ver.is_valid() { + return none + } + return raw_ver.to_version() +} + +fn can_expand(input string) bool { + return input[0] == `~` || + input[0] == `^` || input.contains(hyphen_range_sep) || input.index_any(x_range_symbols) > -1 +} + +fn expand_comparator_set(input string) ?ComparatorSet { + match input[0] { + `~` { return expand_tilda(input[1..]) } + `^` { return expand_caret(input[1..]) } + else {} + } + if input.contains(hyphen_range_sep) { + return expand_hyphen(input) + } + return expand_xrange(input) +} + +fn expand_tilda(raw_version string) ?ComparatorSet { + min_ver := coerce_version(raw_version) or { + return none + } + mut max_ver := min_ver + if min_ver.minor == 0 && min_ver.patch == 0 { + max_ver = min_ver.increment(.major) + } else { + max_ver = min_ver.increment(.minor) + } + return make_comparator_set_ge_lt(min_ver, max_ver) +} + +fn expand_caret(raw_version string) ?ComparatorSet { + min_ver := coerce_version(raw_version) or { + return none + } + mut max_ver := min_ver + if min_ver.major == 0 { + max_ver = min_ver.increment(.minor) + } else { + max_ver = min_ver.increment(.major) + } + return make_comparator_set_ge_lt(min_ver, max_ver) +} + +fn expand_hyphen(raw_range string) ?ComparatorSet { + raw_versions := raw_range.split(hyphen_range_sep) + if raw_versions.len != 2 { + return none + } + min_ver := coerce_version(raw_versions[0]) or { + return none + } + raw_max_ver := parse(raw_versions[1]) + if raw_max_ver.is_missing(ver_major) { + return none + } + mut max_ver := raw_max_ver.coerce() or { + return none + } + if raw_max_ver.is_missing(ver_minor) { + max_ver = max_ver.increment(.minor) + return make_comparator_set_ge_lt(min_ver, max_ver) + } + return make_comparator_set_ge_le(min_ver, max_ver) +} + +fn expand_xrange(raw_range string) ?ComparatorSet { + min_ver := parse_xrange(raw_range) or { + return none + } + if min_ver.major == 0 { + comparators := [ + Comparator{min_ver, Operator.ge}, + ] + return ComparatorSet{comparators} + } + mut max_ver := min_ver + if min_ver.minor == 0 { + max_ver = min_ver.increment(.major) + } else { + max_ver = min_ver.increment(.minor) + } + return make_comparator_set_ge_lt(min_ver, max_ver) +} + +fn make_comparator_set_ge_lt(min Version, max Version) ComparatorSet { + comparators := [ + Comparator{min, Operator.ge}, + Comparator{max, Operator.lt}, + ] + return ComparatorSet{comparators} +} + +fn make_comparator_set_ge_le(min Version, max Version) ComparatorSet { + comparators := [ + Comparator{min, Operator.ge}, + Comparator{max, Operator.le}, + ] + return ComparatorSet{comparators} +} diff --git a/vlib/semver/semver.v b/vlib/semver/semver.v new file mode 100644 index 0000000000..903c21374c --- /dev/null +++ b/vlib/semver/semver.v @@ -0,0 +1,82 @@ +// * Documentation: https://docs.npmjs.com/misc/semver +module semver + +// * Structures. +// Structure representing version in semver format. +pub struct Version { +pub: + major int + minor int + patch int + prerelease string + metadata string +} + +// Enum representing type of version increment. +pub enum Increment { + major + minor + patch +} + +// * Constructor. +// from returns Version structure parsed from input string. +pub fn from(input string) ?Version { + if input.len == 0 { + return error('Empty input') + } + raw_version := parse(input) + version := raw_version.validate() or { + return error('Invalid version format') + } + return version +} + +// build returns Version structure with given major, minor and patch versions. +pub fn build(major int, minor int, patch int) Version { + // TODO Check if versions are greater than zero. + return Version{major, minor, patch, '', ''} +} + +// * Transformation. +// increment returns Version structure with incremented values. +pub fn (ver Version) increment(typ Increment) Version { + return increment_version(ver, typ) +} + +// * Comparison. +pub fn (ver Version) satisfies(input string) bool { + return version_satisfies(ver, input) +} + +pub fn (v1 Version) eq(v2 Version) bool { + return compare_eq(v1, v2) +} + +pub fn (v1 Version) gt(v2 Version) bool { + return compare_gt(v1, v2) +} + +pub fn (v1 Version) lt(v2 Version) bool { + return compare_lt(v1, v2) +} + +pub fn (v1 Version) ge(v2 Version) bool { + return compare_ge(v1, v2) +} + +pub fn (v1 Version) le(v2 Version) bool { + return compare_le(v1, v2) +} + +// * Utilites. +pub fn coerce(input string) ?Version { + ver := coerce_version(input) or { + return error('Invalid version: $input') + } + return ver +} + +pub fn is_valid(input string) bool { + return is_version_valid(input) +} diff --git a/vlib/semver/semver_test.v b/vlib/semver/semver_test.v new file mode 100644 index 0000000000..77c8cd7f1e --- /dev/null +++ b/vlib/semver/semver_test.v @@ -0,0 +1,192 @@ +import semver + +struct TestVersion { + raw string + major int + minor int + patch int + prerelease string + metadata string +} + +struct TestRange { + raw_version string + range_satisfied string + range_unsatisfied string +} + +struct TestCoerce { + invalid string + valid string +} + +const ( + versions_to_test = [ + TestVersion{'1.2.4', 1, 2, 4, '', ''}, + TestVersion{'1.2.4-prerelease-1', 1, 2, 4, 'prerelease-1', ''}, + TestVersion{'1.2.4+20191231', 1, 2, 4, '', '20191231'}, + TestVersion{'1.2.4-prerelease-1+20191231', 1, 2, 4, 'prerelease-1', '20191231'}, + TestVersion{'1.2.4+20191231-prerelease-1', 1, 2, 4, '', '20191231-prerelease-1'}, + ] + ranges_to_test = [ + TestRange{'1.1.0', '1.1.0', '1.1.1'}, + TestRange{'1.1.0', '=1.1.0', '=1.1.1'}, + TestRange{'1.1.0', '>=1.0.0', '<1.1.0'}, + TestRange{'1.1.0', '>=1.0.0 <=1.1.0', '>=1.0.0 <1.1.0'}, + TestRange{'2.3.1', '>=1.0.0 <=1.1.0 || >2.0.0 <2.3.4', '>=1.0.0 <1.1.0'}, + TestRange{'2.3.1', '>=1.0.0 <=1.1.0 || >2.0.0 <2.3.4', '>=1.0.0 <1.1.0 || >4.0.0 <5.0.0'}, + TestRange{'2.3.1', '~2.3.0', '~2.4.0'}, + TestRange{'3.0.0', '~3.0.0', '~4.0.0'}, + TestRange{'2.3.1', '^2.0.0', '^2.4.0'}, + TestRange{'0.3.1', '^0.3.0', '^2.4.0'}, + TestRange{'0.0.4', '^0.0.1', '^0.1.0'}, + TestRange{'2.3.4', '^0.0.1 || ^2.3.0', '^3.1.0 || ^4.2.0'}, + TestRange{'2.3.4', '>2 || <3', '>3 || >4'}, + TestRange{'2.3.4', '2.3.4 - 2.3.5', '2.5.1 - 2.8.3'}, + TestRange{'2.3.4', '2.2 - 2.3', '2.4 - 2.8'}, + TestRange{'2.3.4', '2.3.x', '2.4.x'}, + TestRange{'2.3.4', '2.x', '3.x'}, + TestRange{'2.3.4', '*', '3.x'}, + ] + coerce_to_test = [ + TestCoerce{'1.2.0.4', '1.2.0'}, + TestCoerce{'1.2.0', '1.2.0'}, + TestCoerce{'1.2', '1.2.0'}, + TestCoerce{'1', '1.0.0'}, + TestCoerce{'1-alpha', '1.0.0-alpha'}, + TestCoerce{'1+meta', '1.0.0+meta'}, + TestCoerce{'1-alpha+meta', '1.0.0-alpha+meta'}, + ] + invalid_versions_to_test = [ + 'a.b.c', + '1.2', + '1.2.x', + '1.2.3.4', + '1.2.3-alpha@', + '1.2.3+meta%', + ] + invalid_ranges_to_test = [ + '^a', + '~b', + 'a - c', + '>a', + 'a', + 'a.x', + ] +) + +fn test_from() { + for item in versions_to_test { + ver := semver.from(item.raw) or { + assert false + return + } + assert ver.major == item.major + assert ver.minor == item.minor + assert ver.patch == item.patch + assert ver.metadata == item.metadata + assert ver.prerelease == item.prerelease + } + for ver in invalid_versions_to_test { + semver.from(ver) or { + assert true + continue + } + assert false + } +} + +fn test_increment() { + version1 := semver.build(1, 2, 3) + version1_inc := version1.increment(.major) + assert version1_inc.major == 2 + assert version1_inc.minor == 0 + assert version1_inc.patch == 0 + version2_inc := version1.increment(.minor) + assert version2_inc.major == 1 + assert version2_inc.minor == 3 + assert version2_inc.patch == 0 + version3_inc := version1.increment(.patch) + assert version3_inc.major == 1 + assert version3_inc.minor == 2 + assert version3_inc.patch == 4 +} + +fn test_compare() { + first := semver.build(1, 0, 0) + patch := semver.build(1, 0, 1) + minor := semver.build(1, 2, 3) + major := semver.build(2, 0, 0) + assert first.le(first) + assert first.ge(first) + assert !first.lt(first) + assert !first.gt(first) + assert patch.ge(first) + assert first.le(patch) + assert !first.ge(patch) + assert !patch.le(first) + assert patch.gt(first) + assert first.lt(patch) + assert !first.gt(patch) + assert !patch.lt(first) + assert minor.gt(patch) + assert patch.lt(minor) + assert !patch.gt(minor) + assert !minor.lt(patch) + assert major.gt(minor) + assert minor.lt(major) + assert !minor.gt(major) + assert !major.lt(minor) +} + +fn test_satisfies() { + for item in ranges_to_test { + ver := semver.from(item.raw_version) or { + assert false + return + } + assert ver.satisfies(item.range_satisfied) + assert !ver.satisfies(item.range_unsatisfied) + } +} + +fn test_satisfies_invalid() { + ver := semver.from('1.0.0') or { + assert false + return + } + for item in invalid_ranges_to_test { + assert ver.satisfies(item) == false + } +} + +fn test_coerce() { + for item in coerce_to_test { + valid := semver.from(item.valid) or { + assert false + return + } + fixed := semver.coerce(item.invalid) or { + assert false + return + } + assert fixed.eq(valid) + } +} + +fn test_coerce_invalid() { + semver.coerce('a') or { + assert true + return + } + assert false +} + +fn test_is_valid() { + for item in versions_to_test { + assert semver.is_valid(item.raw) + } + for item in invalid_versions_to_test { + assert semver.is_valid(item) == false + } +} diff --git a/vlib/semver/util.v b/vlib/semver/util.v new file mode 100644 index 0000000000..3674844541 --- /dev/null +++ b/vlib/semver/util.v @@ -0,0 +1,57 @@ +module semver + +// * Private functions. +[inline] +fn is_version_valid(input string) bool { + raw_ver := parse(input) + return raw_ver.is_valid() +} + +[inline] +fn coerce_version(input string) ?Version { + raw_ver := parse(input) + ver := raw_ver.coerce() or { + return error('Invalid version: $input') + } + return ver +} + +[inline] +fn increment_version(ver Version, typ Increment) Version { + mut major := ver.major + mut minor := ver.minor + mut patch := ver.patch + match typ { + .major { + major++ + minor = 0 + patch = 0 + } + .minor { + minor++ + patch = 0 + } + .patch { + patch++ + } + } + return Version{major, minor, patch, ver.prerelease, ver.metadata} +} + +fn is_valid_string(input string) bool { + for c in input { + if !(c.is_letter() || c.is_digit() || c == `.` || c == `-`) { + return false + } + } + return true +} + +fn is_valid_number(input string) bool { + for c in input { + if !c.is_digit() { + return false + } + } + return true +} diff --git a/vlib/semver/v.mod b/vlib/semver/v.mod new file mode 100644 index 0000000000..6e0382c5cc --- /dev/null +++ b/vlib/semver/v.mod @@ -0,0 +1,5 @@ +Module { + name: 'semver' + version: '0.3.0' + deps: [] +} diff --git a/vlib/v/checker/checker.v b/vlib/v/checker/checker.v index 71f1f62894..a364ba398b 100644 --- a/vlib/v/checker/checker.v +++ b/vlib/v/checker/checker.v @@ -8,6 +8,7 @@ import v.token import v.pref import v.util import v.errors +import pkgconfig const ( max_nr_errors = 300 @@ -2477,6 +2478,20 @@ fn (mut c Checker) hash_stmt(mut node ast.HashStmt) { c.error('including C files should use either `"header_file.h"` or `` quoting', node.pos) } + } else if node.kind == 'pkgconfig' { + args := if node.main.contains('--') { node.main.split(' ') } else { '--cflags --libs $node.main'.split(' ') } + mut m := pkgconfig.main(args) or { + c.error(err, node.pos) + return + } + cflags := m.run() or { + c.error(err, node.pos) + return + } + c.table.parse_cflag(cflags, c.mod, c.pref.compile_defines_all) or { + c.error(err, node.pos) + return + } } else if node.kind == 'flag' { // #flag linux -lm mut flag := node.main @@ -2498,7 +2513,8 @@ fn (mut c Checker) hash_stmt(mut node ast.HashStmt) { } } else { if node.kind != 'define' { - c.warn('expected `#include`, `#flag` or `#define` not $node.val', node.pos) + c.warn('expected `#define`, `#flag`, `#include` or `#pkgconfig` not $node.val', + node.pos) } } }