Skip to content

Commit 3b1299c

Browse files
committed
Add support for local paths to libraries input
The previous behavior of the action is to always install the repository as a library. That works for compilation testing of libraries, but there is no reason to restrict this tool to that application. It can just as well be a general purpose tool for compilation testing of any Arduino project (library, platform, sketch). When seen from that perspective, it is better to give the user control over this behavior. For backwards compatibility, the previous behavior of always installing the repository as a library is retained when the old space-separated list syntax is used, or if the user doesn't provide a libraries input.
1 parent d8c807f commit 3b1299c

File tree

4 files changed

+118
-30
lines changed

4 files changed

+118
-30
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ For 3rd party boards, also specify the Boards Manager URL:
2020

2121
YAML-format list of library dependencies to install.
2222

23+
Default `"- source-path: ./"`. This causes the repository to be installed as a library. If there are no library dependencies and you want to override the default, set the `libraries` input to an empty list (`- libraries: '-'`).
24+
2325
Note: the original space-separated list format is also supported. When this syntax is used, the repository under test will always be installed as a library.
2426

2527
#### Sources:
@@ -30,6 +32,12 @@ Keys:
3032
- `name` - name of the library.
3133
- `version` - version of the library to install. Default is the latest version.
3234

35+
##### Local path
36+
37+
Keys:
38+
- `source-path` - path to install as a library. Relative paths are assumed to be relative to the root of the repository.
39+
- `destination-name` - folder name to install the library to. By default, the folder will be named according to the source repository or subfolder name.
40+
3341

3442
### `sketch-paths`
3543

action.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ inputs:
88
description: 'Full qualified board name, with Boards Manager URL if needed'
99
default: 'arduino:avr:uno'
1010
libraries:
11-
default: ''
1211
description: 'YAML-format list of library dependencies to install'
12+
default: '- source-path: ./'
1313
sketch-paths:
1414
description: 'List of paths containing sketches to compile.'
1515
default: 'examples'

compilesketches/compilesketches.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ class RunCommandOutput(enum.Enum):
8888

8989
dependency_name_key = "name"
9090
dependency_version_key = "version"
91+
dependency_source_path_key = "source-path"
92+
dependency_destination_name_key = "destination-name"
9193

9294
latest_release_indicator = "latest"
9395

@@ -241,7 +243,12 @@ def sort_dependency_list(self, dependency_list):
241243
sorted_dependencies = self.Dependencies()
242244
for dependency in dependency_list:
243245
if dependency is not None:
244-
sorted_dependencies.manager.append(dependency)
246+
if self.dependency_source_path_key in dependency:
247+
# Libraries with source-path and no source-url are assumed to be paths
248+
sorted_dependencies.path.append(dependency)
249+
else:
250+
# All others are Library/Board Manager names
251+
sorted_dependencies.manager.append(dependency)
245252

246253
return sorted_dependencies
247254

@@ -250,6 +257,7 @@ class Dependencies:
250257

251258
def __init__(self):
252259
self.manager = []
260+
self.path = []
253261

254262
def install_platforms_from_board_manager(self, platform_list, additional_url_list):
255263
"""Install platform dependencies from the Arduino Board Manager
@@ -372,13 +380,15 @@ def install_libraries(self):
372380
library_list.manager = [{self.dependency_name_key: library_name}
373381
for library_name in parse_list_input(self.libraries)]
374382

383+
# The original behavior of the action was to assume the root of the repo is a library to be installed, so
384+
# that behavior is retained when using the old input syntax
385+
library_list.path = [{self.dependency_source_path_key: pathlib.Path(os.environ["GITHUB_WORKSPACE"])}]
386+
375387
if len(library_list.manager) > 0:
376388
self.install_libraries_from_library_manager(library_list=library_list.manager)
377389

378-
# TODO: this is in anticipation of allowing arbitrary paths to be specified by the user, rather than assuming
379-
# there is a library in the root of the repo (not the case when testing platforms and sketches)
380-
# Install the repository as a library by creating a symlink in the sketchbook
381-
self.install_libraries_from_path(library_path_list=[pathlib.Path(os.environ["GITHUB_WORKSPACE"])])
390+
if len(library_list.path) > 0:
391+
self.install_libraries_from_path(library_list=library_list.path)
382392

383393
def install_libraries_from_library_manager(self, library_list):
384394
"""Install libraries using the Arduino Library Manager
@@ -390,18 +400,37 @@ def install_libraries_from_library_manager(self, library_list):
390400
lib_install_command.extend([self.get_manager_dependency_name(library) for library in library_list])
391401
self.run_arduino_cli_command(command=lib_install_command, enable_output=self.get_run_command_output_level())
392402

393-
def install_libraries_from_path(self, library_path_list):
403+
def install_libraries_from_path(self, library_list):
394404
"""Install libraries from local paths
395405
396406
Keyword arguments:
397-
library_path_list -- list of paths to install libraries from. Relative paths are assumed to be relative to the
398-
Docker container workspace (root of the repository)
407+
library_list -- list of dictionaries defining the dependencies
399408
"""
400-
self.verbose_print("Installing libraries from paths:", list_to_string(library_path_list))
401-
for library_path in library_path_list:
402-
# Install the repository as a library by creating a symlink in the sketchbook
403-
library_symlink_path = self.libraries_path.joinpath(library_path.name)
404-
library_symlink_path.symlink_to(target=library_path, target_is_directory=True)
409+
for library in library_list:
410+
source_path = absolute_path(library[self.dependency_source_path_key])
411+
self.verbose_print("Installing library from path:", path_relative_to_workspace(source_path))
412+
413+
if not source_path.exists():
414+
print("::error::Library source path:", path_relative_to_workspace(source_path), "doesn't exist")
415+
sys.exit(1)
416+
417+
# Determine library folder name (important because it is a factor in dependency resolution)
418+
if self.dependency_destination_name_key in library:
419+
# If a name was specified, use it
420+
destination_name = library[self.dependency_destination_name_key]
421+
elif (
422+
source_path == pathlib.Path(os.environ["GITHUB_WORKSPACE"])
423+
):
424+
# If source_path is the root of the workspace (i.e., repository root), name the folder according to the
425+
# repository name, otherwise it will unexpectedly be "workspace"
426+
destination_name = os.environ["GITHUB_REPOSITORY"].split(sep="/")[1]
427+
else:
428+
# Use the existing folder name
429+
destination_name = source_path.name
430+
431+
# Install the library by creating a symlink in the sketchbook
432+
library_symlink_path = self.libraries_path.joinpath(destination_name)
433+
library_symlink_path.symlink_to(target=source_path, target_is_directory=True)
405434

406435
def find_sketches(self):
407436
"""Return a list of all sketches under the paths specified in the sketch paths list recursively."""

compilesketches/tests/test_compilesketches.py

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ def test_install_platforms(mocker, fqbn_arg, expected_platform, expected_additio
308308
@pytest.mark.parametrize(
309309
"dependency_list, expected_dependency_type_list",
310310
[([None], []),
311+
([{compilesketches.CompileSketches.dependency_source_path_key: "foo/bar"}], ["path"]),
311312
([{compilesketches.CompileSketches.dependency_name_key: "FooBar"}], ["manager"])]
312313
)
313314
def test_sort_dependency_list(monkeypatch, dependency_list, expected_dependency_type_list):
@@ -455,16 +456,22 @@ def test_get_manager_dependency_name(dependency, expected_name):
455456
assert compile_sketches.get_manager_dependency_name(dependency=dependency) == expected_name
456457

457458

458-
@pytest.mark.parametrize("libraries, expected_manager",
459-
[("", []),
460-
("foo bar", [{compilesketches.CompileSketches.dependency_name_key: "foo"},
461-
{compilesketches.CompileSketches.dependency_name_key: "bar"}]),
462-
("\"foo\" \"bar\"", [{compilesketches.CompileSketches.dependency_name_key: "foo"},
463-
{compilesketches.CompileSketches.dependency_name_key: "bar"}]),
464-
("-", []),
465-
("- " + compilesketches.CompileSketches.dependency_name_key + ": foo",
466-
[{compilesketches.CompileSketches.dependency_name_key: "foo"}])])
467-
def test_install_libraries(monkeypatch, mocker, libraries, expected_manager):
459+
@pytest.mark.parametrize(
460+
"libraries, expected_manager, expected_path",
461+
[("", [], [{compilesketches.CompileSketches.dependency_source_path_key: pathlib.PurePath("/foo/GitHubWorkspace")}]),
462+
("foo bar", [{compilesketches.CompileSketches.dependency_name_key: "foo"},
463+
{compilesketches.CompileSketches.dependency_name_key: "bar"}],
464+
[{compilesketches.CompileSketches.dependency_source_path_key: pathlib.PurePath("/foo/GitHubWorkspace")}]),
465+
("\"foo\" \"bar\"", [{compilesketches.CompileSketches.dependency_name_key: "foo"},
466+
{compilesketches.CompileSketches.dependency_name_key: "bar"}],
467+
[{compilesketches.CompileSketches.dependency_source_path_key: pathlib.PurePath("/foo/GitHubWorkspace")}]),
468+
("-", [], []),
469+
("- " + compilesketches.CompileSketches.dependency_name_key + ": foo",
470+
[{compilesketches.CompileSketches.dependency_name_key: "foo"}], []),
471+
("- " + compilesketches.CompileSketches.dependency_source_path_key + ": /foo/bar", [],
472+
[{compilesketches.CompileSketches.dependency_source_path_key: "/foo/bar"}])]
473+
)
474+
def test_install_libraries(monkeypatch, mocker, libraries, expected_manager, expected_path):
468475
libraries_path = pathlib.Path("/foo/LibrariesPath")
469476

470477
monkeypatch.setenv("GITHUB_WORKSPACE", "/foo/GitHubWorkspace")
@@ -488,7 +495,12 @@ def test_install_libraries(monkeypatch, mocker, libraries, expected_manager):
488495
else:
489496
compile_sketches.install_libraries_from_library_manager.assert_not_called()
490497

491-
compile_sketches.install_libraries_from_path.assert_called_once()
498+
if len(expected_path) > 0:
499+
compile_sketches.install_libraries_from_path.assert_called_once_with(
500+
compile_sketches,
501+
library_list=expected_path)
502+
else:
503+
compile_sketches.install_libraries_from_path.assert_not_called()
492504

493505

494506
def test_install_libraries_from_library_manager(mocker):
@@ -509,17 +521,56 @@ def test_install_libraries_from_library_manager(mocker):
509521
enable_output=run_command_output_level)
510522

511523

512-
def test_install_libraries_from_path(mocker):
513-
library_path_list = [pathlib.Path("FooLibrary"), pathlib.Path("bar/Barlibrary")]
524+
@pytest.mark.parametrize("path_exists, library_list, expected_destination_name_list",
525+
[(False, [{compilesketches.CompileSketches.dependency_source_path_key: pathlib.Path(
526+
"/foo/GitHubWorkspace/Nonexistent")}], []),
527+
(True, [{compilesketches.CompileSketches.dependency_destination_name_key: "FooName",
528+
compilesketches.CompileSketches.dependency_source_path_key: pathlib.Path(
529+
"/foo/GitHubWorkspace/FooLibrary")}], ["FooName"]),
530+
(True, [{compilesketches.CompileSketches.dependency_source_path_key: pathlib.Path(
531+
"/foo/GitHubWorkspace")}], ["FooRepoName"]),
532+
(True,
533+
[{compilesketches.CompileSketches.dependency_source_path_key: pathlib.Path(
534+
"/foo/GitHubWorkspace/Bar")}], ["Bar"])])
535+
def test_install_libraries_from_path(capsys, monkeypatch, mocker, path_exists, library_list,
536+
expected_destination_name_list):
537+
libraries_path = pathlib.Path("/foo/LibrariesPath")
538+
symlink_source_path = pathlib.Path("/foo/SymlinkSourcePath")
539+
540+
monkeypatch.setenv("GITHUB_WORKSPACE", "/foo/GitHubWorkspace")
541+
monkeypatch.setenv("GITHUB_REPOSITORY", "foo/FooRepoName")
514542

515543
compile_sketches = get_compilesketches_object()
544+
compile_sketches.libraries_path = libraries_path
516545

546+
mocker.patch.object(pathlib.Path, "exists", autospec=True, return_value=path_exists)
547+
mocker.patch.object(pathlib.Path, "joinpath", autospec=True, return_value=symlink_source_path)
517548
mocker.patch.object(pathlib.Path, "symlink_to", autospec=True)
518549

519-
compile_sketches.install_libraries_from_path(library_path_list=library_path_list)
550+
if not path_exists:
551+
with pytest.raises(expected_exception=SystemExit, match="1"):
552+
compile_sketches.install_libraries_from_path(library_list=library_list)
553+
assert capsys.readouterr().out.strip() == (
554+
"::error::Library source path: "
555+
+ str(compilesketches.path_relative_to_workspace(library_list[0]["source-path"]))
556+
+ " doesn't exist"
557+
)
520558

521-
# noinspection PyUnresolvedReferences
522-
pathlib.Path.symlink_to.assert_called()
559+
else:
560+
compile_sketches.install_libraries_from_path(library_list=library_list)
561+
562+
joinpath_calls = []
563+
symlink_to_calls = []
564+
for library, expected_destination_name in zip(library_list, expected_destination_name_list):
565+
joinpath_calls.append(unittest.mock.call(libraries_path, expected_destination_name))
566+
symlink_to_calls.append(
567+
unittest.mock.call(symlink_source_path,
568+
target=library[compilesketches.CompileSketches.dependency_source_path_key],
569+
target_is_directory=True))
570+
571+
# noinspection PyUnresolvedReferences
572+
pathlib.Path.joinpath.assert_has_calls(calls=joinpath_calls)
573+
pathlib.Path.symlink_to.assert_has_calls(calls=symlink_to_calls)
523574

524575

525576
def test_find_sketches(capsys, monkeypatch):

0 commit comments

Comments
 (0)