Skip to content

Commit 1979ae5

Browse files
committed
Add support for installing library dependencies from repositories
Previously, library dependencies could only be installed via Library Manager. - It is often desirable to test against non-release versions of dependencies. - Releases take some time to be picked up by the Library Manager indexer and propagated through the CDN cache to become available for installation via LM. - Some libraries are not available from Library Manager. The optional source-path field allows installing libraries from a subfolder of the repository. The optional destination-name field allows installing to a custom library name. This is useful to perfectly emulate the Library Manager installation of the library (which names the folder according to the library.properties name field).
1 parent 2a39928 commit 1979ae5

File tree

4 files changed

+309
-10
lines changed

4 files changed

+309
-10
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ Keys:
3838
- `source-path` - path to install as a library. Relative paths are assumed to be relative to the root of the repository.
3939
- `destination-name` - folder name to install the library to. By default, the folder will be named according to the source repository or subfolder name.
4040

41+
##### Repository
42+
43+
Keys:
44+
- `source-url` - URL to clone the repository from. It must start with `git://` or end with `.git`.
45+
- `version` - [Git ref](https://git-scm.com/book/en/v2/Git-Internals-Git-References) of the repository to checkout. The special version name `latest` will cause the latest tag to be used. By default, the repository will be checked out to the tip of the default branch.
46+
- `source-path` - path to install as a library. Paths are relative to the root of the repository. The default is to install from the root of the repository.
47+
- `destination-name` - folder name to install the library to. By default, the folder will be named according to the source repository or subfolder name.
48+
4149

4250
### `sketch-paths`
4351

compilesketches/compilesketches.py

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pathlib
66
import re
77
import shlex
8+
import shutil
89
import subprocess
910
import sys
1011
import tarfile
@@ -13,6 +14,7 @@
1314
import urllib.request
1415

1516
import git
17+
import gitdb.exc
1618
import github
1719
import yaml
1820
import yaml.parser
@@ -89,6 +91,7 @@ class RunCommandOutput(enum.Enum):
8991
dependency_name_key = "name"
9092
dependency_version_key = "version"
9193
dependency_source_path_key = "source-path"
94+
dependency_source_url_key = "source-url"
9295
dependency_destination_name_key = "destination-name"
9396

9497
latest_release_indicator = "latest"
@@ -243,7 +246,14 @@ def sort_dependency_list(self, dependency_list):
243246
sorted_dependencies = self.Dependencies()
244247
for dependency in dependency_list:
245248
if dependency is not None:
246-
if self.dependency_source_path_key in dependency:
249+
if self.dependency_source_url_key in dependency:
250+
# Repositories are identified by the URL starting with git:// or ending in .git
251+
if (
252+
dependency[self.dependency_source_url_key].rstrip("/").endswith(".git")
253+
or dependency[self.dependency_source_url_key].startswith("git://")
254+
):
255+
sorted_dependencies.repository.append(dependency)
256+
elif self.dependency_source_path_key in dependency:
247257
# Libraries with source-path and no source-url are assumed to be paths
248258
sorted_dependencies.path.append(dependency)
249259
else:
@@ -258,6 +268,7 @@ class Dependencies:
258268
def __init__(self):
259269
self.manager = []
260270
self.path = []
271+
self.repository = []
261272

262273
def install_platforms_from_board_manager(self, platform_list, additional_url_list):
263274
"""Install platform dependencies from the Arduino Board Manager
@@ -360,6 +371,77 @@ def run_command(self, command, enable_output=RunCommandOutput.ON_FAILURE, exit_o
360371

361372
return command_data
362373

374+
def get_repository_dependency_ref(self, dependency):
375+
"""Return the appropriate git ref value for a repository dependency
376+
377+
Keyword arguments:
378+
dependency -- dictionary defining the repository dependency
379+
"""
380+
if self.dependency_version_key in dependency:
381+
git_ref = dependency[self.dependency_version_key]
382+
else:
383+
git_ref = None
384+
385+
return git_ref
386+
387+
def install_from_repository(self, url, git_ref, source_path, destination_parent_path, destination_name=None):
388+
"""Install by cloning a repository
389+
390+
Keyword arguments:
391+
url -- URL to download the archive from
392+
git_ref -- the Git ref (e.g., branch, tag, commit) to checkout after cloning
393+
source_path -- path relative to the root of the repository to install from
394+
destination_parent_path -- path under which to install
395+
destination_name -- folder name to use for the installation. Set to None to use the repository name.
396+
(default None)
397+
"""
398+
if destination_name is None:
399+
if source_path.rstrip("/") == ".":
400+
# Use the repository name
401+
destination_name = url.rstrip("/").rsplit(sep="/", maxsplit=1)[1].rsplit(sep=".", maxsplit=1)[0]
402+
else:
403+
# Use the source path folder name
404+
destination_name = pathlib.PurePath(source_path).name
405+
406+
if source_path.rstrip("/") == ".":
407+
# Clone directly to the target path
408+
self.clone_repository(url=url, git_ref=git_ref,
409+
destination_path=pathlib.PurePath(destination_parent_path, destination_name))
410+
else:
411+
# Clone to a temporary folder
412+
with tempfile.TemporaryDirectory() as clone_folder:
413+
self.clone_repository(url=url, git_ref=git_ref, destination_path=clone_folder)
414+
# Install by moving the source folder
415+
shutil.move(src=str(pathlib.PurePath(clone_folder, source_path)),
416+
dst=str(pathlib.PurePath(destination_parent_path, destination_name)))
417+
418+
def clone_repository(self, url, git_ref, destination_path):
419+
"""Clone a Git repository to a specified location and check out the specified ref
420+
421+
Keyword arguments:
422+
git_ref -- Git ref to check out. Set to None to leave repository checked out at the tip of the default branch.
423+
destination_path -- destination for the cloned repository. This is the full path of the repository, not the
424+
parent path.
425+
"""
426+
if git_ref is None:
427+
# Shallow clone is only possible if using the tip of the branch
428+
clone_arguments = {"depth": 1}
429+
else:
430+
clone_arguments = {}
431+
cloned_repository = git.Repo.clone_from(url=url, to_path=destination_path, **clone_arguments)
432+
if git_ref is not None:
433+
if git_ref == self.latest_release_indicator:
434+
# "latest" may be used in place of a ref to cause a checkout of the latest tag
435+
try:
436+
# Check if there is a real ref named "latest", in which case it will be used
437+
cloned_repository.rev_parse(git_ref)
438+
except gitdb.exc.BadName:
439+
# There is no real ref named "latest", so checkout latest (associated with most recent commit) tag
440+
git_ref = sorted(cloned_repository.tags, key=lambda tag: tag.commit.committed_date)[-1]
441+
442+
# checkout ref
443+
cloned_repository.git.checkout(git_ref)
444+
363445
def install_libraries(self):
364446
"""Install Arduino libraries."""
365447
self.libraries_path.mkdir(parents=True, exist_ok=True)
@@ -390,6 +472,9 @@ def install_libraries(self):
390472
if len(library_list.path) > 0:
391473
self.install_libraries_from_path(library_list=library_list.path)
392474

475+
if len(library_list.repository) > 0:
476+
self.install_libraries_from_repository(library_list=library_list.repository)
477+
393478
def install_libraries_from_library_manager(self, library_list):
394479
"""Install libraries using the Arduino Library Manager
395480
@@ -432,6 +517,36 @@ def install_libraries_from_path(self, library_list):
432517
library_symlink_path = self.libraries_path.joinpath(destination_name)
433518
library_symlink_path.symlink_to(target=source_path, target_is_directory=True)
434519

520+
def install_libraries_from_repository(self, library_list):
521+
"""Install libraries by cloning Git repositories
522+
523+
Keyword arguments:
524+
library_list -- list of dictionaries defining the dependencies
525+
"""
526+
for library in library_list:
527+
self.verbose_print("Installing library from repository:", library[self.dependency_source_url_key])
528+
529+
# Determine library folder name (important because it is a factor in dependency resolution)
530+
if self.dependency_destination_name_key in library:
531+
# If a folder name was specified, use it
532+
destination_name = library[self.dependency_destination_name_key]
533+
else:
534+
# None will cause the repository name to be used by install_from_repository()
535+
destination_name = None
536+
537+
git_ref = self.get_repository_dependency_ref(dependency=library)
538+
539+
if self.dependency_source_path_key in library:
540+
source_path = library[self.dependency_source_path_key]
541+
else:
542+
source_path = "."
543+
544+
self.install_from_repository(url=library[self.dependency_source_url_key],
545+
git_ref=git_ref,
546+
source_path=source_path,
547+
destination_parent_path=self.libraries_path,
548+
destination_name=destination_name)
549+
435550
def find_sketches(self):
436551
"""Return a list of all sketches under the paths specified in the sketch paths list recursively."""
437552
sketch_list = []

0 commit comments

Comments
 (0)