Skip to content

Commit a3d1048

Browse files
iboBTheLartians
andauthored
Infer package name and version from URL (#220)
* Function to extract name and version from url. Some tests * Rewrite. Previous version was not safe enough. More tests * Allow underscore as a name-version separator (<name>_<ver>) * CPMAddPackage can infer name and version from url * Allow URL parse from single arg and uncomment tests * Info about shorthand syntax in README * Fix style * Fixed typo Co-authored-by: Lars Melchior <TheLartians@users.noreply.github.com> * Explicit hash algorithm in shorthand URL example. Also added tests which include a hash algorithm provided We can't document a default until it's confirmed here: https://gitlab.kitware.com/cmake/cmake/-/issues/21859 Co-authored-by: Lars Melchior <TheLartians@users.noreply.github.com>
1 parent 492e762 commit a3d1048

File tree

4 files changed

+149
-18
lines changed

4 files changed

+149
-18
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ CPMAddPackage("uri@version#tag")
6262

6363
In the shorthand syntax if the URI is of the form `gh:user/name`, it is interpreted as GitHub URI and converted to `https://github.com/user/name.git`. If the URI is of the form `gl:user/name`, it is interpreted as a [GitLab](https://gitlab.com/explore/) URI and coverted to `https://gitlab.com/user/name.git`. Otherwise the URI used verbatim as a git URL. All packages added using the shorthand syntax will be added using the [EXCLUDE_FROM_ALL](https://cmake.org/cmake/help/latest/prop_tgt/EXCLUDE_FROM_ALL.html) flag.
6464

65+
The single-argument syntax also works for URLs:
66+
67+
```cmake
68+
# An archive package from a given url. The version is inferred
69+
CPMAddPackage("https://example.com/my-package-1.2.3.zip")
70+
# An archive package from a given url with an MD5 hash provided
71+
CPMAddPackage("https://example.com/my-package-1.2.3.zip#MD5=68e20f674a48be38d60e129f600faf7d")
72+
# An archive package from a given url. The version is explicitly given
73+
CPMAddPackage("https://example.com/my-package.zip@1.2.3")
74+
```
75+
6576
After calling `CPMAddPackage` or `CPMFindPackage`, the following variables are defined in the local scope, where `<dependency>` is the name of the dependency.
6677

6778
- `<dependency>_SOURCE_DIR` is the path to the source of the dependency.

cmake/CPM.cmake

Lines changed: 66 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ endif()
134134
include(FetchContent)
135135
include(CMakeParseArguments)
136136

137-
# Infer package name from git repository uri (path or url)
137+
# Try to infer package name from git repository uri (path or url)
138138
function(cpm_package_name_from_git_uri URI RESULT)
139139
if("${URI}" MATCHES "([^/:]+)/?.git/?$")
140140
set(${RESULT}
@@ -146,6 +146,52 @@ function(cpm_package_name_from_git_uri URI RESULT)
146146
endif()
147147
endfunction()
148148

149+
# Try to infer package name and version from a url
150+
function(cpm_package_name_and_ver_from_url url outName outVer)
151+
if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)")
152+
# We matched an archive
153+
set(filename "${CMAKE_MATCH_1}")
154+
155+
if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)")
156+
# We matched <name>-<version> (ie foo-1.2.3)
157+
set(${outName}
158+
"${CMAKE_MATCH_1}"
159+
PARENT_SCOPE
160+
)
161+
set(${outVer}
162+
"${CMAKE_MATCH_2}"
163+
PARENT_SCOPE
164+
)
165+
elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)")
166+
# We couldn't find a name, but we found a version
167+
#
168+
# In many cases (which we don't handle here) the url would look something like
169+
# `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly
170+
# distinguish the package name from the irrelevant bits. Moreover if we try to match the
171+
# package name from the filename, we'd get bogus at best.
172+
unset(${outName} PARENT_SCOPE)
173+
set(${outVer}
174+
"${CMAKE_MATCH_1}"
175+
PARENT_SCOPE
176+
)
177+
else()
178+
# Boldly assume that the file name is the package name.
179+
#
180+
# Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but
181+
# such cases should be quite rare. No popular service does this... we think.
182+
set(${outName}
183+
"${filename}"
184+
PARENT_SCOPE
185+
)
186+
unset(${outVer} PARENT_SCOPE)
187+
endif()
188+
else()
189+
# No ideas yet what to do with non-archives
190+
unset(${outName} PARENT_SCOPE)
191+
unset(${outVer} PARENT_SCOPE)
192+
endif()
193+
endfunction()
194+
149195
# Initialize logging prefix
150196
if(NOT CPM_INDENT)
151197
set(CPM_INDENT
@@ -274,11 +320,6 @@ function(cpm_parse_add_package_single_arg arg outArgs)
274320
set(out "GIT_REPOSITORY;${arg}")
275321
set(packageType "git")
276322
else()
277-
# This error here is temporary. We can't provide URLs from here until we support inferring the
278-
# package name from an url. When this is supported, remove this error as well as commented out
279-
# tests in test/unit/parse_add_package_single_arg.cmake
280-
message(FATAL_ERROR "CPM: Unsupported package type of '${arg}'")
281-
282323
# Fall back to a URL
283324
set(out "URL;${arg}")
284325
set(packageType "archive")
@@ -349,7 +390,7 @@ function(CPMAddPackage)
349390
EXCLUDE_FROM_ALL
350391
)
351392

352-
set(multiValueArgs OPTIONS)
393+
set(multiValueArgs URL OPTIONS)
353394

354395
cmake_parse_arguments(CPM_ARGS "" "${oneValueArgs}" "${multiValueArgs}" "${ARGN}")
355396

@@ -397,6 +438,24 @@ function(CPMAddPackage)
397438
endif()
398439
endif()
399440

441+
if(DEFINED CPM_ARGS_URL)
442+
# If a name or version aren't provided, try to infer them from the URL
443+
list(GET CPM_ARGS_URL 0 firstUrl)
444+
cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl)
445+
# If we fail to obtain name and version from the first URL, we could try other URLs if any.
446+
# However multiple URLs are expected to be quite rare, so for now we won't bother.
447+
448+
# If the caller provided their own name and version, they trump the inferred ones.
449+
if(NOT DEFINED CPM_ARGS_NAME)
450+
set(CPM_ARGS_NAME ${nameFromUrl})
451+
endif()
452+
if(NOT DEFINED CPM_ARGS_VERSION)
453+
set(CPM_ARGS_VERSION ${verFromUrl})
454+
endif()
455+
456+
list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}")
457+
endif()
458+
400459
# Check for required arguments
401460

402461
if(NOT DEFINED CPM_ARGS_NAME)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
cmake_minimum_required(VERSION 3.14 FATAL_ERROR)
2+
3+
include(${CPM_PATH}/CPM.cmake)
4+
include(${CPM_PATH}/testing.cmake)
5+
6+
cpm_package_name_and_ver_from_url("https://example.com/coolpack-1.2.3.zip" name ver)
7+
assert_equal("coolpack" ${name})
8+
assert_equal("1.2.3" ${ver})
9+
10+
cpm_package_name_and_ver_from_url("https://example.com/cool-pack-v1.3.tar.gz" name ver)
11+
assert_equal("cool-pack" ${name})
12+
assert_equal("1.3" ${ver})
13+
14+
cpm_package_name_and_ver_from_url(
15+
"https://subd.zip.com/download.php?Cool.Pack-v1.2.3rc0.tar" name ver
16+
)
17+
assert_equal("Cool.Pack" ${name})
18+
assert_equal("1.2.3rc0" ${ver})
19+
20+
cpm_package_name_and_ver_from_url(
21+
"http://evil-1.2.tar.gz.com/Plan9_1.2.3a.tar.bz2?download" name ver
22+
)
23+
assert_equal("Plan9" ${name})
24+
assert_equal("1.2.3a" ${ver})
25+
26+
cpm_package_name_and_ver_from_url(
27+
"http://evil-1.2.tar.gz.com/Plan_9-1.2.3a.tar.bz2?download" name ver
28+
)
29+
assert_equal("Plan_9" ${name})
30+
assert_equal("1.2.3a" ${ver})
31+
32+
cpm_package_name_and_ver_from_url(
33+
"http://evil-1.2.tar.gz.com/Plan-9_1.2.3a.tar.bz2?download" name ver
34+
)
35+
assert_equal("Plan-9" ${name})
36+
assert_equal("1.2.3a" ${ver})
37+
38+
cpm_package_name_and_ver_from_url("https://sf.com/distrib/SFLib-0.999.4.tar.gz/download" name ver)
39+
assert_equal("SFLib" ${name})
40+
assert_equal("0.999.4" ${ver})
41+
42+
cpm_package_name_and_ver_from_url("https://example.com/coolpack/v5.6.5rc44.zip" name ver)
43+
assert_not_defined(name)
44+
assert_equal("5.6.5rc44" ${ver})
45+
46+
cpm_package_name_and_ver_from_url("evil-1.3.zip.com/coolpack/release999.000beta.ZIP" name ver)
47+
assert_not_defined(name)
48+
assert_equal("999.000beta" ${ver})
49+
50+
cpm_package_name_and_ver_from_url("https://example.com/Foo55.tar.gz" name ver)
51+
assert_equal("Foo55" ${name})
52+
assert_not_defined(ver)
53+
54+
cpm_package_name_and_ver_from_url("https://example.com/foo" name ver)
55+
assert_not_defined(name)
56+
assert_not_defined(ver)
57+
58+
cpm_package_name_and_ver_from_url("example.zip.com/foo" name ver)
59+
assert_not_defined(name)
60+
assert_not_defined(ver)

test/unit/parse_add_package_single_arg.cmake

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -47,18 +47,19 @@ assert_equal(
4747
"${args}"
4848
)
4949

50-
# The following test cases are to be used in the future, once single-argument archives are supported
50+
cpm_parse_add_package_single_arg("https://example.org/foo.tar.gz" args)
51+
assert_equal("URL;https://example.org/foo.tar.gz" "${args}")
5152

52-
# cpm_parse_add_package_single_arg("https://example.org/foo.tar.gz" args)
53+
cpm_parse_add_package_single_arg("https://example.org/foo.tar.gz#baadf00d@1.2.0" args)
54+
assert_equal("URL;https://example.org/foo.tar.gz;URL_HASH;baadf00d;VERSION;1.2.0" "${args}")
5355

54-
# assert_equal("URL;https://example.org/foo.tar.gz" "${args}")
56+
cpm_parse_add_package_single_arg("https://example.org/foo.tar.gz#MD5=baadf00d" args)
57+
assert_equal("URL;https://example.org/foo.tar.gz;URL_HASH;MD5=baadf00d" "${args}")
5558

56-
# cpm_parse_add_package_single_arg("https://example.org/foo.tar.gz#baadf00d@1.2.0" args)
59+
cpm_parse_add_package_single_arg("https://example.org/Foo.zip#SHA3_512=1337" args)
60+
assert_equal("URL;https://example.org/Foo.zip;URL_HASH;SHA3_512=1337" "${args}")
5761

58-
# assert_equal("URL;https://example.org/foo.tar.gz;URL_HASH;baadf00d;VERSION;1.2.0" "${args}")
59-
60-
# cpm_parse_add_package_single_arg("ftp://user:password@server/pathname.zip#fragment#0ddb411@0"
61-
# args)
62-
63-
# assert_equal("URL;ftp://user:password@server/pathname.zip#fragment;URL_HASH;0ddb411;VERSION;0"
64-
# "${args}")
62+
cpm_parse_add_package_single_arg("ftp://user:pass@server/pathname.zip#fragment#0ddb411@0" args)
63+
assert_equal(
64+
"URL;ftp://user:pass@server/pathname.zip#fragment;URL_HASH;0ddb411;VERSION;0" "${args}"
65+
)

0 commit comments

Comments
 (0)