def _file_from_label(l): files = l.files.to_list() if len(files) != 1: fail(msg = "Unexpected number of files in target {}: {}".format(l, len(files))) return files[0] def _get_common_prefix(paths): if not paths: return "" prefix = paths[0].split("/") for path in paths[1:]: path_components = path.split("/") common_length = min(len(prefix), len(path_components)) prefix = [prefix[i] for i in range(common_length) if prefix[i] == path_components[i]] if not prefix: break return "/".join(prefix) def _remove_prefix(path, path_prefix): path_parts = path.split("/") for path_prefix_part in path_prefix.split("/"): if path_parts[0] != path_prefix_part: fail("{} is not prefixed by {}".format(path, path_prefix)) path_parts.pop(0) return "/".join(path_parts) # Finds the relative path needed to create a symlink between the link file at # `link_path` to the link target at `link_target_path`. # # Example: # _get_relative_path_for_link( # link_target_path = "cuttlefish-common/bin/graphics_detector", # link_path = "cuttlefish-common/bin/aarch64-linux-gnu/gfxstream_graphics_detector", # ) == "../graphics_detector" def _get_relative_path_for_link(link_target_path, link_path): common_path = _get_common_prefix([link_target_path, link_path]) relative_link_path = _remove_prefix(link_path, common_path) relative_link_path_depth = len(relative_link_path.split("/")) - 1 relative_link_target_path = _remove_prefix(link_target_path, common_path) return "".join(["../" for i in range(relative_link_path_depth)]) + relative_link_target_path def _package_files_impl(ctx): default_outputs = list() path_to_declared_file = dict() for (dst, src) in ctx.attr.package_file_to_src.items(): out_file = ctx.actions.declare_file(dst) path_to_declared_file[dst] = out_file if dst == ctx.attr.executable: executable = out_file default_outputs.append(out_file) input_file = _file_from_label(src) ctx.actions.run_shell( mnemonic = "MakeOutputDir", outputs = [out_file], inputs = [input_file], command = "mkdir -p " + out_file.dirname + " && cp " + input_file.path + " " + out_file.path, ) for (dst, target) in ctx.attr.package_file_symlink_to_package_file.items(): dst_file = ctx.actions.declare_file(dst) default_outputs.append(dst_file) src_file = path_to_declared_file.get(target) if src_file == None: fail(msg = "Package file \"{}\" in \"package_file_symlink_to_package_file\" does not exist as a key in \"package_file_to_src\"".format(target)) relative_target = _get_relative_path_for_link(target, dst) # https://github.com/bazelbuild/bazel/issues/14224: `ctx.actions.symlink()` is not used # here because that appears to a link using the absolute path whereas links relative to # the package are desired here. ctx.actions.run_shell( mnemonic = "MakeOutputDir", outputs = [dst_file], inputs = [src_file], command = "mkdir -p " + dst_file.dirname + " && ln -s " + relative_target + " " + dst_file.path, ) return [ DefaultInfo( executable = executable, files = depset(default_outputs), ), ] package_files = rule( attrs = { "executable": attr.string(), "package_file_to_src": attr.string_keyed_label_dict( doc = "File paths within the generated package archive to the target providing the file.", allow_files = True), "package_file_symlink_to_package_file": attr.string_dict( doc = """File path within the generated package archive for a symlink pointing to another package file created by "package_file_to_src".""", ), }, executable = True, implementation = _package_files_impl, )