| #!/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() |