diff --git a/docs/paths.md b/docs/paths.md index 6b1a31bb5..869e585ca 100644 --- a/docs/paths.md +++ b/docs/paths.md @@ -10,7 +10,14 @@ Public API relative_file(to_file, frm_file) -Resolves a relative path between two files, "to_file" and "frm_file", they must share the same root +Resolves a relative path between two files, "to_file" and "frm_file". + +If neither of the paths begin with ../ it is assumed that they share the same root. When finding the relative path, +the incoming files are treated as actual files (not folders) so the resulting relative path may differ when compared +to passing the same arguments to python's "os.path.relpath()" or NodeJs's "path.relative()". + +For example, 'relative_file("../foo/foo.txt", "bar/bar.txt")' will return '../../foo/foo.txt' + **PARAMETERS** diff --git a/lib/private/paths.bzl b/lib/private/paths.bzl index cd2ee9094..f824b9ebf 100644 --- a/lib/private/paths.bzl +++ b/lib/private/paths.bzl @@ -3,7 +3,13 @@ load("@bazel_skylib//lib:paths.bzl", _spaths = "paths") def _relative_file(to_file, frm_file): - """Resolves a relative path between two files, "to_file" and "frm_file", they must share the same root + """Resolves a relative path between two files, "to_file" and "frm_file". + + If neither of the paths begin with ../ it is assumed that they share the same root. When finding the relative path, + the incoming files are treated as actual files (not folders) so the resulting relative path may differ when compared + to passing the same arguments to python's "os.path.relpath()" or NodeJs's "path.relative()". + + For example, 'relative_file("../foo/foo.txt", "bar/bar.txt")' will return '../../foo/foo.txt' Args: to_file: the path with file name to resolve to, from frm @@ -13,32 +19,40 @@ def _relative_file(to_file, frm_file): The relative path from frm_file to to_file, including the file name """ + to_parent_count = to_file.count("../") + frm_parent_count = frm_file.count("../") + + parent_count = to_parent_count + + if to_parent_count > 0 and frm_parent_count > 0: + if frm_parent_count > to_parent_count: + fail("traversing more parent directories (with '../') for 'frm_file' than 'to_file' requires file layout knowledge") + + parent_count = to_parent_count - frm_parent_count + to_segments = _spaths.normalize(_spaths.join("/", to_file)).split("/")[:-1] frm_segments = _spaths.normalize(_spaths.join("/", frm_file)).split("/")[:-1] if len(to_segments) == 0 and len(frm_segments) == 0: return to_file - if to_segments[0] != frm_segments[0]: - msg = "paths must share a common root, got '{}' and '{}'".format(to_file, frm_file) - fail(msg) - - longest_common = [] - for to_seg, frm_seg in zip(to_segments, frm_segments): - if to_seg == frm_seg: - longest_common.append(to_seg) - else: - break + # since we prefix a "/" and normalize, the first segment is always "". So split point will be at least 1. + split_point = 1 - split_point = len(longest_common) + # If either of the paths starts with ../ then assume that any shared paths are a coincidence. + if to_segments[0] != ".." and frm_segments != "..": + longest_common = [] + for to_seg, frm_seg in zip(to_segments, frm_segments): + if to_seg == frm_seg: + longest_common.append(to_seg) + else: + break - if split_point == 0: - msg = "paths share no common ancestor, '{}' -> '{}'".format(frm_file, to_file) - fail(msg) + split_point = len(longest_common) return _spaths.join( *( - [".."] * (len(frm_segments) - split_point) + + [".."] * (len(frm_segments) - split_point + parent_count) + to_segments[split_point:] + [_spaths.basename(to_file)] ) diff --git a/lib/tests/paths_test.bzl b/lib/tests/paths_test.bzl index ca4ec8ff5..ebc40d75a 100644 --- a/lib/tests/paths_test.bzl +++ b/lib/tests/paths_test.bzl @@ -114,6 +114,78 @@ def _relative_file_test_impl(ctx): ), ) + asserts.equals( + env, + "../../../../../repo/some/external/repo/short/path.txt", + paths.relative_file( + "../repo/some/external/repo/short/path.txt", + "some/main/repo/short/path.txt", + ), + ) + + asserts.equals( + env, + "../../../../../../../../repo/some/external/repo/short/path.txt", + paths.relative_file( + "../../../../repo/some/external/repo/short/path.txt", + "some/main/repo/short/path.txt", + ), + ) + + asserts.equals( + env, + "../../../../repo/some/external/repo/short/path.txt", + paths.relative_file( + "repo/some/external/repo/short/path.txt", + "../some/main/repo/short/path.txt", + ), + ) + + asserts.equals( + env, + "../../../../repo/some/external/repo/short/path.txt", + paths.relative_file( + "repo/some/external/repo/short/path.txt", + "../../../../some/main/repo/short/path.txt", + ), + ) + + asserts.equals( + env, + "../../../../../repo/some/external/repo/short/path.txt", + paths.relative_file( + "../../repo/some/external/repo/short/path.txt", + "../some/main/repo/short/path.txt", + ), + ) + + asserts.equals( + env, + "../../../../../../../repo/some/external/repo/short/path.txt", + paths.relative_file( + "../../../../repo/some/external/repo/short/path.txt", + "../some/main/repo/short/path.txt", + ), + ) + + asserts.equals( + env, + "../../../../repo/some/external/repo/short/path.txt", + paths.relative_file( + "../repo/some/external/repo/short/path.txt", + "../some/main/repo/short/path.txt", + ), + ) + + asserts.equals( + env, + "../../../../repo/some/external/repo/short/path.txt", + paths.relative_file( + "../../../repo/some/external/repo/short/path.txt", + "../../../some/main/repo/short/path.txt", + ), + ) + return unittest.end(env) def _manifest_path_test_impl(ctx):