| #!/usr/bin/env fuchsia-vendored-python |
| # Copyright 2023 The Fuchsia Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| import os |
| import sys |
| import tempfile |
| import unittest |
| |
| from pathlib import Path |
| from unittest import mock |
| from typing import Iterable, Sequence |
| |
| import linker |
| import cl_utils |
| |
| |
| class TryLinkerScriptTextTests(unittest.TestCase): |
| def test_empty_text(self): |
| text = "" |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| text_file = tdp / "foo.so" |
| text_file.write_text(text) |
| self.assertEqual(linker.try_linker_script_text(text_file), text) |
| |
| def test_nonempty_text(self): |
| text = "INPUT(libfoo.so.4)\n" |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| text_file = tdp / "libbar.so" |
| text_file.write_text(text) |
| self.assertEqual(linker.try_linker_script_text(text_file), text) |
| |
| def test_binary_file(self): |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| bin_file = tdp / "libbar.so" |
| bin_file.write_bytes(b"\xd0\xff\xfe\x07") |
| self.assertIsNone(linker.try_linker_script_text(bin_file)) |
| |
| def test_archive_header(self): |
| text = b"!<arch>xyzxyzxyz" |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| text_file = tdp / "libarxiv.a" |
| text_file.write_bytes(text) |
| self.assertIsNone(linker.try_linker_script_text(text_file)) |
| |
| def test_elf_header(self): |
| text = b"\x7fELF-on-the-shelf" |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| text_file = tdp / "libbar.so" |
| text_file.write_bytes(text) |
| self.assertIsNone(linker.try_linker_script_text(text_file)) |
| |
| def test_macho_header(self): |
| text = b"\xca\xfe\xba\xbe\x01\x02" |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| text_file = tdp / "libmar.dylib" |
| text_file.write_bytes(text) |
| self.assertIsNone(linker.try_linker_script_text(text_file)) |
| |
| def test_dll_header(self): |
| text = b"\x5a\x4d\xee\xaa\xee\xaa" |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| text_file = tdp / "libzzz.dll" |
| text_file.write_bytes(text) |
| self.assertIsNone(linker.try_linker_script_text(text_file)) |
| |
| |
| class LinkerScriptParseTests(unittest.TestCase): |
| def test_empty(self): |
| with tempfile.TemporaryDirectory() as td: |
| link = linker.LinkerInvocation(working_dir_abs=Path(td)) |
| |
| self.assertEqual(link.search_paths, []) |
| self.assertEqual(link.l_libs, []) |
| self.assertEqual(link.direct_files, []) |
| self.assertIsNone(link.sysroot) |
| |
| expanded = list(link.expand_linker_script("")) |
| |
| self.assertEqual(link.search_paths, []) |
| self.assertEqual(link.l_libs, []) |
| self.assertEqual(link.direct_files, []) |
| self.assertIsNone(link.sysroot) |
| self.assertEqual(expanded, []) |
| |
| def test_nothing_but_space(self): |
| with tempfile.TemporaryDirectory() as td: |
| link = linker.LinkerInvocation(working_dir_abs=Path(td)) |
| |
| expanded = list(link.expand_linker_script("\n \n \n \n")) |
| self.assertEqual(expanded, []) |
| |
| def test_comment_one_line(self): |
| with tempfile.TemporaryDirectory() as td: |
| link = linker.LinkerInvocation(working_dir_abs=Path(td)) |
| |
| expanded = list(link.expand_linker_script("/* single-line */\n")) |
| self.assertEqual(expanded, []) |
| |
| def test_comment_multi_line(self): |
| with tempfile.TemporaryDirectory() as td: |
| link = linker.LinkerInvocation(working_dir_abs=Path(td)) |
| |
| expanded = list(link.expand_linker_script("/* multi\n\nline */\n")) |
| self.assertEqual(expanded, []) |
| |
| def test_output_ignored(self): |
| with tempfile.TemporaryDirectory() as td: |
| link = linker.LinkerInvocation(working_dir_abs=Path(td)) |
| |
| expanded = list(link.expand_linker_script("OUTPUT(libignored.a)\n")) |
| self.assertEqual(expanded, []) |
| |
| def test_target_ignored(self): |
| with tempfile.TemporaryDirectory() as td: |
| link = linker.LinkerInvocation(working_dir_abs=Path(td)) |
| |
| expanded = list(link.expand_linker_script("TARGET(acquired)\n")) |
| self.assertEqual(expanded, []) |
| |
| def test_output_format_ignored(self): |
| with tempfile.TemporaryDirectory() as td: |
| link = linker.LinkerInvocation(working_dir_abs=Path(td)) |
| |
| expanded = list(link.expand_linker_script("OUTPUT_FORMAT(half-elf)\n")) |
| self.assertEqual(expanded, []) |
| |
| def test_search_dir(self): |
| dir1 = Path("/some/where/here") |
| dir2 = Path("/some/where/there") |
| with tempfile.TemporaryDirectory() as td: |
| link = linker.LinkerInvocation(working_dir_abs=Path(td)) |
| |
| expanded = list( |
| link.expand_linker_script(f"SEARCH_DIR({dir1})\nSEARCH_DIR({dir2})") |
| ) |
| self.assertEqual(expanded, []) |
| self.assertEqual(link.search_paths, [dir1, dir2]) # kept in-order |
| |
| def test_one_input_with_extension(self): |
| libdir = Path("baz/lib/foo") |
| lib = Path("libbar.so.1") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| link = linker.LinkerInvocation(working_dir_abs=tdp) |
| with mock.patch.object( |
| linker.LinkerInvocation, |
| "resolve_path", |
| return_value=libdir / lib, |
| ) as mock_resolve: |
| expanded = list(link.expand_linker_script(f"INPUT({lib})\n")) |
| |
| mock_resolve.assert_called_with(lib, check_sysroot=True) |
| self.assertEqual(expanded, [libdir / lib]) |
| |
| def test_one_input_with_extension_space_insensitive(self): |
| libdir = Path("baz/lib/foo") |
| lib = Path("libbar.so.1") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| link = linker.LinkerInvocation(working_dir_abs=tdp) |
| with mock.patch.object( |
| linker.LinkerInvocation, |
| "resolve_path", |
| return_value=libdir / lib, |
| ) as mock_resolve: |
| expanded = list( |
| link.expand_linker_script(f"INPUT (\n {lib}\n)\n") |
| ) |
| |
| mock_resolve.assert_called_with(lib, check_sysroot=True) |
| self.assertEqual(expanded, [libdir / lib]) |
| |
| def test_one_input_without_extension(self): |
| libdir = Path("baz/lib/foo") |
| lib = Path("libbar.so") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| link = linker.LinkerInvocation(working_dir_abs=tdp) |
| with mock.patch.object( |
| linker.LinkerInvocation, |
| "resolve_lib", |
| return_value=libdir / lib, |
| ) as mock_resolve: |
| expanded = list(link.expand_linker_script(f"INPUT( -lbar )\n")) |
| |
| mock_resolve.assert_called_with("bar") |
| self.assertEqual(expanded, [libdir / lib]) |
| |
| def test_input_multple_with_extension_with_comma(self): |
| libdir1 = Path("baz/lib/foo") |
| lib1 = Path("libbar.so.1") |
| libdir2 = Path("qqq/rarlib") |
| lib2 = Path("libzzz.so") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, search_paths=[libdir1, libdir2] |
| ) |
| with mock.patch.object( |
| linker.LinkerInvocation, |
| "resolve_path", |
| side_effect=[libdir1 / lib1, libdir2 / lib2], |
| ) as mock_resolve: |
| # comma is optional |
| expanded = list( |
| link.expand_linker_script(f"INPUT({lib1}, {lib2})\n") |
| ) |
| |
| mock_resolve.assert_has_calls( |
| [ |
| mock.call(lib1, check_sysroot=True), |
| mock.call(lib2, check_sysroot=True), |
| ] |
| ) |
| self.assertEqual(expanded, [libdir1 / lib1, libdir2 / lib2]) |
| |
| def test_input_multple_with_extension_without_comma(self): |
| libdir1 = Path("baz/lib/foo") |
| lib1 = Path("libbar.so.1") |
| libdir2 = Path("qqq/rarlib") |
| lib2 = Path("libzzz.so") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, search_paths=[libdir1, libdir2] |
| ) |
| with mock.patch.object( |
| linker.LinkerInvocation, |
| "resolve_path", |
| side_effect=[libdir1 / lib1, libdir2 / lib2], |
| ) as mock_resolve: |
| # comma is optional |
| expanded = list( |
| link.expand_linker_script(f"INPUT( {lib1} {lib2} )\n") |
| ) |
| |
| mock_resolve.assert_has_calls( |
| [ |
| mock.call(lib1, check_sysroot=True), |
| mock.call(lib2, check_sysroot=True), |
| ] |
| ) |
| self.assertEqual(expanded, [libdir1 / lib1, libdir2 / lib2]) |
| |
| def test_group_input_with_extension(self): |
| libdir = Path("baz/lib") |
| lib = Path("libfar.so.1") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| link = linker.LinkerInvocation(working_dir_abs=tdp) |
| with mock.patch.object( |
| linker.LinkerInvocation, |
| "resolve_path", |
| return_value=libdir / lib, |
| ) as mock_resolve: |
| expanded = list(link.expand_linker_script(f"GROUP({lib})\n")) |
| |
| mock_resolve.assert_called_with(lib, check_sysroot=True) |
| self.assertEqual(expanded, [libdir / lib]) |
| |
| def test_as_needed_with_extension(self): |
| libdir = Path("baz/lib/foo") |
| lib = Path("libbar.so.1") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, search_paths=[libdir] |
| ) |
| with mock.patch.object( |
| linker.LinkerInvocation, |
| "resolve_path", |
| return_value=libdir / lib, |
| ) as mock_resolve: |
| expanded = list( |
| link.expand_linker_script(f"INPUT( AS_NEEDED({lib}) )\n") |
| ) |
| |
| mock_resolve.assert_called_with(lib, check_sysroot=True) |
| self.assertEqual(expanded, [libdir / lib]) |
| |
| def test_as_needed_multple_with_extension_without_comma(self): |
| libdir1 = Path("baz/lib/foo") |
| lib1 = Path("libbar.so.1") |
| libdir2 = Path("qqq/rarlib") |
| lib2 = Path("libzzz.so") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, search_paths=[libdir1, libdir2] |
| ) |
| with mock.patch.object( |
| linker.LinkerInvocation, |
| "resolve_path", |
| side_effect=[libdir1 / lib1, libdir2 / lib2], |
| ) as mock_resolve: |
| # comma is optional |
| expanded = list( |
| link.expand_linker_script( |
| f"INPUT( AS_NEEDED({lib1}) AS_NEEDED({lib2}) )\n" |
| ) |
| ) |
| |
| mock_resolve.assert_has_calls( |
| [ |
| mock.call(lib1, check_sysroot=True), |
| mock.call(lib2, check_sysroot=True), |
| ] |
| ) |
| self.assertEqual(expanded, [libdir1 / lib1, libdir2 / lib2]) |
| |
| def test_include_empty(self): |
| libdir = Path("baz/lib/foo") |
| lib = Path("libincludeme.so") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / libdir).mkdir(parents=True, exist_ok=True) |
| (tdp / libdir / lib).write_text(f"/* empty */\n") |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, search_paths=[libdir] |
| ) |
| expanded = list(link.expand_linker_script(f"INCLUDE {lib}\n")) |
| |
| self.assertEqual(expanded, [libdir / lib]) |
| |
| def test_include_with_lib(self): |
| libdir = Path("baz/lib/foo") |
| lib1 = Path("libincludeme.so") |
| lib2 = Path("libbar.so.1") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / libdir).mkdir(parents=True, exist_ok=True) |
| (tdp / libdir / lib1).write_text(f"INPUT({lib2})\n") |
| (tdp / libdir / lib2).touch() |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, search_paths=[libdir] |
| ) |
| expanded = list(link.expand_linker_script(f"INCLUDE {lib1}\n")) |
| |
| self.assertEqual(expanded, [libdir / lib1, libdir / lib2]) |
| |
| def test_include_nested_with_lib(self): |
| libdir = Path("baz/lib/foo") |
| lib1 = Path("libincludeme.so") |
| lib2 = Path("libforward.so") |
| lib3 = Path("libbar.so.1") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / libdir).mkdir(parents=True, exist_ok=True) |
| (tdp / libdir / lib1).write_text(f"INCLUDE {lib2}\n") |
| (tdp / libdir / lib2).write_text(f"INPUT ( {lib3} )\n") |
| (tdp / libdir / lib3).touch() |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, search_paths=[libdir] |
| ) |
| expanded = list(link.expand_linker_script(f"INCLUDE {lib1}\n")) |
| |
| self.assertEqual( |
| expanded, [libdir / lib1, libdir / lib2, libdir / lib3] |
| ) |
| |
| |
| class LinkerInvocationResolveTests(unittest.TestCase): |
| def test_resolve_path_failure(self): |
| libdir = Path("baz/lib/foo") |
| sysroot = Path("quuz/sysroot") |
| lib = Path("libbar.so.1") |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, |
| search_paths=[libdir], |
| sysroot=sysroot, |
| ) |
| resolved = link.resolve_path(lib, check_sysroot=True) |
| |
| self.assertIsNone(resolved) |
| |
| def test_resolve_path_success_in_search_path(self): |
| libdir = Path("raz/lib/foo") |
| sysroot = Path("quuz/sysroot") |
| lib = Path("libbar.so.1") |
| |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / libdir).mkdir(parents=True, exist_ok=True) |
| (tdp / libdir / lib).touch() |
| |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, |
| search_paths=[libdir], |
| sysroot=sysroot, |
| ) |
| resolved = link.resolve_path(lib, check_sysroot=True) |
| |
| self.assertEqual(resolved, libdir / lib) |
| |
| def test_resolve_path_success_in_sysroot(self): |
| libdir = Path("raz/lib/foo") |
| sysroot = Path("quuz/sysroot") |
| lib = Path("libbar.so.1") |
| |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / sysroot).mkdir(parents=True, exist_ok=True) |
| (tdp / sysroot / lib).touch() |
| |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, |
| search_paths=[libdir], |
| sysroot=sysroot, |
| ) |
| resolved = link.resolve_path(lib, check_sysroot=True) |
| |
| self.assertEqual(resolved, sysroot / lib) |
| |
| def test_resolve_path_avoiding_sysroot(self): |
| libdir = Path("raz/lib/foo") |
| sysroot = Path("quuz/sysroot") |
| lib = Path("libbar.so.1") |
| |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / sysroot).mkdir(parents=True, exist_ok=True) |
| (tdp / sysroot / lib).touch() |
| |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, |
| search_paths=[libdir], |
| sysroot=sysroot, |
| ) |
| resolved = link.resolve_path(lib, check_sysroot=False) |
| |
| self.assertIsNone(resolved) |
| |
| def test_resolve_lib_failure(self): |
| libdir = Path("snaz/lib") |
| sysroot = Path("bar/root") |
| lib = Path("libfoo.a") |
| |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / libdir).mkdir(parents=True, exist_ok=True) |
| |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, |
| search_paths=[libdir], |
| sysroot=sysroot, |
| ) |
| resolved = link.resolve_lib("foo") |
| |
| self.assertIsNone(resolved) |
| |
| def test_resolve_lib_success_in_search_path(self): |
| libdir = Path("snaz/lib") |
| sysroot = Path("bar/root") |
| lib = Path("libfoo.a") |
| |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / libdir).mkdir(parents=True, exist_ok=True) |
| (tdp / libdir / lib).touch() |
| |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, |
| search_paths=[libdir], |
| sysroot=sysroot, |
| ) |
| resolved = link.resolve_lib("foo") |
| |
| self.assertEqual(resolved, libdir / lib) |
| |
| def test_resolve_lib_success_in_sysroot(self): |
| libdir = Path("snaz/lib") |
| sysroot = Path("bar/root") |
| lib = Path("libzoo.a") |
| |
| with tempfile.TemporaryDirectory() as td: |
| tdp = Path(td) |
| (tdp / sysroot).mkdir(parents=True, exist_ok=True) |
| (tdp / sysroot / lib).touch() |
| |
| link = linker.LinkerInvocation( |
| working_dir_abs=tdp, |
| search_paths=[libdir], |
| sysroot=sysroot, |
| ) |
| resolved = link.resolve_lib("zoo") |
| |
| self.assertEqual(resolved, sysroot / lib) |
| |
| def test_expand_using_lld(self): |
| depfile_text = """/dev/null: \\ |
| libfoo.so \\ |
| libfoo.so.1 |
| |
| # phony deps (to be ignored) |
| libfoo.so: |
| |
| libfoo.so.1: |
| |
| """ |
| link = linker.LinkerInvocation() # not really using parameters |
| with mock.patch.object( |
| cl_utils, |
| "subprocess_call", |
| return_value=cl_utils.SubprocessResult( |
| 0, stdout=depfile_text.splitlines() |
| ), |
| ) as mock_call: |
| deps = list( |
| link.expand_using_lld( |
| lld=Path("/opt/bin/ld.lld"), inputs=[Path("libfoo.so")] |
| ) |
| ) |
| |
| self.assertEqual(deps, [Path("libfoo.so"), Path("libfoo.so.1")]) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |