Skip to content

Commit

Permalink
Infer package name and version from URL (#220)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>
  • Loading branch information
iboB and TheLartians authored Feb 23, 2021
1 parent 492e762 commit a3d1048
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 18 deletions.
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,17 @@ CPMAddPackage("uri@version#tag")

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.

The single-argument syntax also works for URLs:

```cmake
# An archive package from a given url. The version is inferred
CPMAddPackage("https://example.com/my-package-1.2.3.zip")
# An archive package from a given url with an MD5 hash provided
CPMAddPackage("https://example.com/my-package-1.2.3.zip#MD5=68e20f674a48be38d60e129f600faf7d")
# An archive package from a given url. The version is explicitly given
CPMAddPackage("https://example.com/[email protected]")
```

After calling `CPMAddPackage` or `CPMFindPackage`, the following variables are defined in the local scope, where `<dependency>` is the name of the dependency.

- `<dependency>_SOURCE_DIR` is the path to the source of the dependency.
Expand Down
73 changes: 66 additions & 7 deletions cmake/CPM.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,7 @@ endif()
include(FetchContent)
include(CMakeParseArguments)

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

# Try to infer package name and version from a url
function(cpm_package_name_and_ver_from_url url outName outVer)
if(url MATCHES "[/\\?]([a-zA-Z0-9_\\.-]+)\\.(tar|tar\\.gz|tar\\.bz2|zip|ZIP)(\\?|/|$)")
# We matched an archive
set(filename "${CMAKE_MATCH_1}")

if(filename MATCHES "([a-zA-Z0-9_\\.-]+)[_-]v?(([0-9]+\\.)*[0-9]+[a-zA-Z0-9]*)")
# We matched <name>-<version> (ie foo-1.2.3)
set(${outName}
"${CMAKE_MATCH_1}"
PARENT_SCOPE
)
set(${outVer}
"${CMAKE_MATCH_2}"
PARENT_SCOPE
)
elseif(filename MATCHES "(([0-9]+\\.)+[0-9]+[a-zA-Z0-9]*)")
# We couldn't find a name, but we found a version
#
# In many cases (which we don't handle here) the url would look something like
# `irrelevant/ACTUAL_PACKAGE_NAME/irrelevant/1.2.3.zip`. In such a case we can't possibly
# distinguish the package name from the irrelevant bits. Moreover if we try to match the
# package name from the filename, we'd get bogus at best.
unset(${outName} PARENT_SCOPE)
set(${outVer}
"${CMAKE_MATCH_1}"
PARENT_SCOPE
)
else()
# Boldly assume that the file name is the package name.
#
# Yes, something like `irrelevant/ACTUAL_NAME/irrelevant/download.zip` will ruin our day, but
# such cases should be quite rare. No popular service does this... we think.
set(${outName}
"${filename}"
PARENT_SCOPE
)
unset(${outVer} PARENT_SCOPE)
endif()
else()
# No ideas yet what to do with non-archives
unset(${outName} PARENT_SCOPE)
unset(${outVer} PARENT_SCOPE)
endif()
endfunction()

# Initialize logging prefix
if(NOT CPM_INDENT)
set(CPM_INDENT
Expand Down Expand Up @@ -274,11 +320,6 @@ function(cpm_parse_add_package_single_arg arg outArgs)
set(out "GIT_REPOSITORY;${arg}")
set(packageType "git")
else()
# This error here is temporary. We can't provide URLs from here until we support inferring the
# package name from an url. When this is supported, remove this error as well as commented out
# tests in test/unit/parse_add_package_single_arg.cmake
message(FATAL_ERROR "CPM: Unsupported package type of '${arg}'")

# Fall back to a URL
set(out "URL;${arg}")
set(packageType "archive")
Expand Down Expand Up @@ -349,7 +390,7 @@ function(CPMAddPackage)
EXCLUDE_FROM_ALL
)

set(multiValueArgs OPTIONS)
set(multiValueArgs URL OPTIONS)

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

Expand Down Expand Up @@ -397,6 +438,24 @@ function(CPMAddPackage)
endif()
endif()

if(DEFINED CPM_ARGS_URL)
# If a name or version aren't provided, try to infer them from the URL
list(GET CPM_ARGS_URL 0 firstUrl)
cpm_package_name_and_ver_from_url(${firstUrl} nameFromUrl verFromUrl)
# If we fail to obtain name and version from the first URL, we could try other URLs if any.
# However multiple URLs are expected to be quite rare, so for now we won't bother.

# If the caller provided their own name and version, they trump the inferred ones.
if(NOT DEFINED CPM_ARGS_NAME)
set(CPM_ARGS_NAME ${nameFromUrl})
endif()
if(NOT DEFINED CPM_ARGS_VERSION)
set(CPM_ARGS_VERSION ${verFromUrl})
endif()

list(APPEND CPM_ARGS_UNPARSED_ARGUMENTS URL "${CPM_ARGS_URL}")
endif()

# Check for required arguments

if(NOT DEFINED CPM_ARGS_NAME)
Expand Down
60 changes: 60 additions & 0 deletions test/unit/package_name_and_ver_from_url.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
cmake_minimum_required(VERSION 3.14 FATAL_ERROR)

include(${CPM_PATH}/CPM.cmake)
include(${CPM_PATH}/testing.cmake)

cpm_package_name_and_ver_from_url("https://example.com/coolpack-1.2.3.zip" name ver)
assert_equal("coolpack" ${name})
assert_equal("1.2.3" ${ver})

cpm_package_name_and_ver_from_url("https://example.com/cool-pack-v1.3.tar.gz" name ver)
assert_equal("cool-pack" ${name})
assert_equal("1.3" ${ver})

cpm_package_name_and_ver_from_url(
"https://subd.zip.com/download.php?Cool.Pack-v1.2.3rc0.tar" name ver
)
assert_equal("Cool.Pack" ${name})
assert_equal("1.2.3rc0" ${ver})

cpm_package_name_and_ver_from_url(
"http://evil-1.2.tar.gz.com/Plan9_1.2.3a.tar.bz2?download" name ver
)
assert_equal("Plan9" ${name})
assert_equal("1.2.3a" ${ver})

cpm_package_name_and_ver_from_url(
"http://evil-1.2.tar.gz.com/Plan_9-1.2.3a.tar.bz2?download" name ver
)
assert_equal("Plan_9" ${name})
assert_equal("1.2.3a" ${ver})

cpm_package_name_and_ver_from_url(
"http://evil-1.2.tar.gz.com/Plan-9_1.2.3a.tar.bz2?download" name ver
)
assert_equal("Plan-9" ${name})
assert_equal("1.2.3a" ${ver})

cpm_package_name_and_ver_from_url("https://sf.com/distrib/SFLib-0.999.4.tar.gz/download" name ver)
assert_equal("SFLib" ${name})
assert_equal("0.999.4" ${ver})

cpm_package_name_and_ver_from_url("https://example.com/coolpack/v5.6.5rc44.zip" name ver)
assert_not_defined(name)
assert_equal("5.6.5rc44" ${ver})

cpm_package_name_and_ver_from_url("evil-1.3.zip.com/coolpack/release999.000beta.ZIP" name ver)
assert_not_defined(name)
assert_equal("999.000beta" ${ver})

cpm_package_name_and_ver_from_url("https://example.com/Foo55.tar.gz" name ver)
assert_equal("Foo55" ${name})
assert_not_defined(ver)

cpm_package_name_and_ver_from_url("https://example.com/foo" name ver)
assert_not_defined(name)
assert_not_defined(ver)

cpm_package_name_and_ver_from_url("example.zip.com/foo" name ver)
assert_not_defined(name)
assert_not_defined(ver)
23 changes: 12 additions & 11 deletions test/unit/parse_add_package_single_arg.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,19 @@ assert_equal(
"${args}"
)

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

# cpm_parse_add_package_single_arg("https://example.org/foo.tar.gz" args)
cpm_parse_add_package_single_arg("https://example.org/foo.tar.gz#[email protected]" args)
assert_equal("URL;https://example.org/foo.tar.gz;URL_HASH;baadf00d;VERSION;1.2.0" "${args}")

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

# cpm_parse_add_package_single_arg("https://example.org/foo.tar.gz#[email protected]" args)
cpm_parse_add_package_single_arg("https://example.org/Foo.zip#SHA3_512=1337" args)
assert_equal("URL;https://example.org/Foo.zip;URL_HASH;SHA3_512=1337" "${args}")

# assert_equal("URL;https://example.org/foo.tar.gz;URL_HASH;baadf00d;VERSION;1.2.0" "${args}")

# cpm_parse_add_package_single_arg("ftp://user:password@server/pathname.zip#fragment#0ddb411@0"
# args)

# assert_equal("URL;ftp://user:password@server/pathname.zip#fragment;URL_HASH;0ddb411;VERSION;0"
# "${args}")
cpm_parse_add_package_single_arg("ftp://user:pass@server/pathname.zip#fragment#0ddb411@0" args)
assert_equal(
"URL;ftp://user:pass@server/pathname.zip#fragment;URL_HASH;0ddb411;VERSION;0" "${args}"
)

0 comments on commit a3d1048

Please sign in to comment.