diff --git a/pycheribuild/config/compilation_targets.py b/pycheribuild/config/compilation_targets.py index 0b8e073e9..5b0229ac9 100644 --- a/pycheribuild/config/compilation_targets.py +++ b/pycheribuild/config/compilation_targets.py @@ -144,6 +144,10 @@ def default_install_dir(self, install_dir: DefaultInstallDir) -> Path: return self.sysroot_dir return super().default_install_dir(install_dir) + @classmethod + def _get_csa_project(cls) -> "type[BuildLLVMInterface]": + raise NotImplementedError() + @property def c_compiler(self) -> Path: return self._compiler_dir / "clang" @@ -176,6 +180,18 @@ def nm(self) -> Path: def strip_tool(self) -> Path: return self._compiler_dir / "llvm-strip" + @property + def ccc_analyzer(self) -> Path: + return self._get_csa_project().get_native_install_path(self.config) / "libexec/ccc-analyzer" + + @property + def cxx_analyzer(self) -> Path: + return self._get_csa_project().get_native_install_path(self.config) / "libexec/c++-analyzer" + + @property + def scan_build(self) -> Path: + return self._get_csa_project().get_native_install_path(self.config) / "bin/scan-build" + @classmethod @abstractmethod def triple_for_target( @@ -638,6 +654,10 @@ def _get_run_project(self, xtarget: "CrossCompileTarget", caller: SimpleProject) result = SimpleProject.get_instance_for_target_name("run", xtarget, caller.config, caller) return typing.cast(LaunchFreeBSDInterface, result) + @classmethod + def _get_csa_project(cls) -> "type[BuildLLVMInterface]": + return typing.cast("type[BuildLLVMInterface]", SimpleProject.get_class_for_target_name("cheri-csa", None)) + @classmethod def is_cheribsd(cls) -> bool: return True @@ -725,6 +745,10 @@ def essential_compiler_and_linker_flags_impl(cls, instance: "CheriBSDTargetInfo" result.extend(cheribsd_morello_version_dependent_flags(version, xtarget.is_cheri_purecap())) return result + @classmethod + def _get_csa_project(cls) -> "type[BuildLLVMInterface]": + return typing.cast("type[BuildLLVMInterface]", SimpleProject.get_class_for_target_name("morello-csa", None)) + # FIXME: This is completely wrong since cherios is not cheribsd, but should work for now: class CheriOSTargetInfo(CheriBSDTargetInfo): @@ -1093,6 +1117,14 @@ def nm(self) -> Path: def strip_tool(self) -> Path: return self.bindir / (self.binary_prefix + "strip") + @property + def ccc_analyzer(self) -> Path: + raise NotImplementedError + + @property + def cxx_analyzer(self) -> Path: + raise NotImplementedError + @classmethod def essential_compiler_and_linker_flags_impl(cls, *args, **kwargs) -> "list[str]": # This version of GCC should work without any additional flags diff --git a/pycheribuild/config/target_info.py b/pycheribuild/config/target_info.py index 9aeca978d..feef4294f 100644 --- a/pycheribuild/config/target_info.py +++ b/pycheribuild/config/target_info.py @@ -319,6 +319,14 @@ def nm(self) -> Path: ... @abstractmethod def strip_tool(self) -> Path: ... + @property + @abstractmethod + def ccc_analyzer(self) -> Path: ... + + @property + @abstractmethod + def cxx_analyzer(self) -> Path: ... + @classmethod @abstractmethod def essential_compiler_and_linker_flags_impl( @@ -635,6 +643,14 @@ def nm(self) -> Path: def strip_tool(self) -> Path: return self.c_compiler.parent / "strip" + @property + def ccc_analyzer(self) -> Path: + raise NotImplementedError + + @property + def cxx_analyzer(self) -> Path: + raise NotImplementedError + @classmethod def is_freebsd(cls) -> bool: return OSInfo.IS_FREEBSD diff --git a/pycheribuild/projects/cmake.py b/pycheribuild/projects/cmake.py index 12801c1fe..78a5d7944 100644 --- a/pycheribuild/projects/cmake.py +++ b/pycheribuild/projects/cmake.py @@ -111,6 +111,10 @@ def custom_target_name(base_target: str, xtarget: CrossCompileTarget) -> str: cross_install_dir = DefaultInstallDir.ROOTFS_OPTBASE supported_architectures = CompilationTargets.ALL_SUPPORTED_CHERIBSD_TARGETS + @classmethod + def can_build_with_csa(cls) -> bool: + return True + def linkage(self): # We always want to build the CheriBSD CTest binary static so that we can use in QEMU without needing libuv. assert "libuv" in self.dependencies diff --git a/pycheribuild/projects/cross/cheribsd.py b/pycheribuild/projects/cross/cheribsd.py index 58c7397ba..3193b3b78 100644 --- a/pycheribuild/projects/cross/cheribsd.py +++ b/pycheribuild/projects/cross/cheribsd.py @@ -1005,8 +1005,8 @@ def _setup_cross_toolchain_config(self) -> None: if not xccinfo.is_clang: self.ask_for_confirmation("Cross compiler is not clang, are you sure you want to continue?") self.cross_toolchain_config.set_env( - XCC=self.CC, - XCXX=self.CXX, + XCC=self.cc_wrapper, + XCXX=self.cxx_wrapper, XCPP=self.CPP, X_COMPILER_TYPE=xccinfo.compiler, # This is needed otherwise the build assumes it should build with $CC ) @@ -1196,9 +1196,14 @@ def _buildkernel( # Don't build a compiler if we are using and external toolchain (only build config, etc) if not self.use_bootstrapped_toolchain: kernel_toolchain_opts.set_with_options(LLD_BOOTSTRAP=False, CLANG=False, CLANG_BOOTSTRAP=False) + kernel_toolchain_opts.exclude_from_csa = True self.run_make("kernel-toolchain", options=kernel_toolchain_opts) self.kernel_toolchain_exists = True self.info("Building kernels for configs:", " ".join(kernconfs)) + + if self.use_csa: + kernel_make_args.set(BUILD_WITH_STRICT_TMPPATH=False) # Look for perl in PATH + self.run_make( "buildkernel", options=kernel_make_args, @@ -1523,6 +1528,10 @@ def add_cross_build_options(self) -> None: # links from /usr/bin/mail to /usr/bin/Mail won't work on case-insensitve fs self.make_args.set_with_options(MAIL=False) + if self.use_csa: + ccinfo = self.get_compiler_info(self.host_CC) + self.make_args.set_env(COMPILER_TYPE=ccinfo.compiler) + def libcompat_name(self) -> str: if self.crosscompile_target.is_cheri_purecap(): return "lib64" @@ -1917,6 +1926,14 @@ def setup_config_options(cls, kernel_only_target=False, install_directory_help=N "build the libraries and skip all binaries", ) + @classmethod + def can_build_with_csa(cls) -> bool: + return True + + @classproperty + def extra_scan_build_args(self) -> "list[str]": + return ["-disable-checker", "alpha.core.PointerSub"] + def __init__(self, *args, **kwargs) -> None: super().__init__(*args, **kwargs) self.extra_kernels_with_mfs: "list[str]" = [] diff --git a/pycheribuild/projects/cross/compiler_rt.py b/pycheribuild/projects/cross/compiler_rt.py index 69de3d590..2ad0f8dae 100644 --- a/pycheribuild/projects/cross/compiler_rt.py +++ b/pycheribuild/projects/cross/compiler_rt.py @@ -47,6 +47,10 @@ class BuildCompilerRt(CrossCompileCMakeProject): _check_install_dir_conflict = False supported_architectures = CompilationTargets.ALL_SUPPORTED_CHERIBSD_AND_HOST_TARGETS + @classmethod + def can_build_with_csa(cls) -> bool: + return True + def setup(self): # For the NATIVE variant we want to install to the compiler resource dir: if self.compiling_for_host(): diff --git a/pycheribuild/projects/cross/dlmalloc.py b/pycheribuild/projects/cross/dlmalloc.py index 44d896483..a3ea26f84 100644 --- a/pycheribuild/projects/cross/dlmalloc.py +++ b/pycheribuild/projects/cross/dlmalloc.py @@ -38,6 +38,10 @@ class DLMalloc(CrossCompileProject): make_kind = MakeCommandKind.GnuMake native_install_dir = DefaultInstallDir.CHERI_SDK + @classmethod + def can_build_with_csa(cls) -> bool: + return True + @classmethod def setup_config_options(cls, **kwargs): super().setup_config_options(**kwargs) @@ -119,7 +123,7 @@ def setup(self): self.make_args.set(DEBUG=self.debug) self.make_args.set(CAPREVOKE=self.revoke) self.make_args.set(SRCDIR=self.source_dir) - self.make_args.set_env(CC=self.CC, CFLAGS=commandline_to_str(self.default_compiler_flags + self.CFLAGS)) + self.make_args.set_env(CC=self.cc_wrapper, CFLAGS=commandline_to_str(self.default_compiler_flags + self.CFLAGS)) if not self.compiling_for_host(): self.make_args.set_env(CHERI_SDK=self.target_info.sdk_root_dir) diff --git a/pycheribuild/projects/cross/libcxx.py b/pycheribuild/projects/cross/libcxx.py index 3cd70c84a..10e930a1c 100644 --- a/pycheribuild/projects/cross/libcxx.py +++ b/pycheribuild/projects/cross/libcxx.py @@ -66,6 +66,10 @@ class BuildLibCXXRT(_CxxRuntimeCMakeProject): repository = GitRepository("https://github.com/CTSRD-CHERI/libcxxrt.git") supported_architectures = CompilationTargets.ALL_SUPPORTED_CHERIBSD_AND_BAREMETAL_AND_HOST_TARGETS + @classmethod + def can_build_with_csa(cls) -> bool: + return True + @classmethod def dependencies(cls, config: CheriConfig) -> "tuple[str, ...]": result = super().dependencies(config) diff --git a/pycheribuild/projects/cross/llvm.py b/pycheribuild/projects/cross/llvm.py index 1d6bb46bc..c60f8cc0a 100644 --- a/pycheribuild/projects/cross/llvm.py +++ b/pycheribuild/projects/cross/llvm.py @@ -871,3 +871,43 @@ def update(self): src_dir=self.source_dir / "tools/lldb", revision=self.lldb_revision, ) + + +class BuildCheriLLVMWithCSA(BuildCheriLLVM): + repository = GitRepository( + "https://github.com/rems-project/llvm-project.git", force_branch=True, default_branch="cheri-csa" + ) + default_directory_basename = "cheri-csa" + target = "cheri-csa" + _default_install_dir_fn = ComputedDefaultValue( + function=lambda config, project: config.output_root / "cheri-csa", as_string="$INSTALL_ROOT/cheri-csa" + ) + + skip_misc_llvm_tools = True + included_projects = ["clang"] + skip_static_analyzer = False + hide_options_from_help = True + + @classmethod + def get_native_install_path(cls, config: CheriConfig): + return config.output_root / "cheri-csa" + + +class BuildMorelloLLVMWithCSA(BuildMorelloLLVM): + repository = GitRepository( + "https://github.com/rems-project/llvm-project.git", force_branch=True, default_branch="morello-csa-llvm-14" + ) + default_directory_basename = "morello-csa" + target = "morello-csa" + _default_install_dir_fn = ComputedDefaultValue( + function=lambda config, project: config.output_root / "morello-csa", as_string="$INSTALL_ROOT/morello-csa" + ) + + skip_misc_llvm_tools = True + included_projects = ["clang"] + skip_static_analyzer = False + hide_options_from_help = True + + @classmethod + def get_native_install_path(cls, config: CheriConfig): + return config.output_root / "morello-csa" diff --git a/pycheribuild/projects/cross/newlib.py b/pycheribuild/projects/cross/newlib.py index 4fa5446a3..78b7c9c5d 100644 --- a/pycheribuild/projects/cross/newlib.py +++ b/pycheribuild/projects/cross/newlib.py @@ -58,6 +58,10 @@ def custom_target_name(base_target: str, xtarget: CrossCompileTarget) -> str: return base_target + "-baremetal-" + xtarget.base_arch_suffix return base_target + "-" + xtarget.generic_target_suffix + @classmethod + def can_build_with_csa(cls) -> bool: + return True + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) assert self._install_prefix == Path("/", self.target_info.target_triple) @@ -99,8 +103,8 @@ def setup(self): bindir = self.sdk_bindir self.add_configure_vars( AS_FOR_TARGET=str(self.CC), # + target_cflags, - CC_FOR_TARGET=str(self.CC), # + target_cflags, - CXX_FOR_TARGET=str(self.CXX), # + target_cflags, + CC_FOR_TARGET=str(self.cc_wrapper), # + target_cflags, + CXX_FOR_TARGET=str(self.cxx_wrapper), # + target_cflags, AR_FOR_TARGET=self.target_info.ar, STRIP_FOR_TARGET=self.target_info.strip_tool, OBJCOPY_FOR_TARGET=bindir / "objcopy", diff --git a/pycheribuild/projects/cross/sqlite.py b/pycheribuild/projects/cross/sqlite.py index 60c855def..32eb1a9e1 100644 --- a/pycheribuild/projects/cross/sqlite.py +++ b/pycheribuild/projects/cross/sqlite.py @@ -35,6 +35,10 @@ class BuildSQLite(CrossCompileAutotoolsProject): "https://github.com/CTSRD-CHERI/sqlite.git", default_branch="3.22.0-cheri", force_branch=True ) + @classmethod + def can_build_with_csa(cls) -> bool: + return True + def check_system_dependencies(self) -> None: super().check_system_dependencies() # XXX: Disabling amalgamation should remove the requirement for tclsh, but it seems the build still invokes it. diff --git a/pycheribuild/projects/cross/zlib.py b/pycheribuild/projects/cross/zlib.py index 978be83a0..5635b5f35 100644 --- a/pycheribuild/projects/cross/zlib.py +++ b/pycheribuild/projects/cross/zlib.py @@ -34,6 +34,10 @@ class BuildZlib(CrossCompileAutotoolsProject): repository = GitRepository("https://github.com/CTSRD-CHERI/zlib.git") + @classmethod + def can_build_with_csa(cls) -> bool: + return True + # Enable the same hacks as nginx since this isn't really autoconf... add_host_target_build_config_options = False _configure_understands_enable_static = False diff --git a/pycheribuild/projects/project.py b/pycheribuild/projects/project.py index 15f2f1d2e..7eafcc2ba 100644 --- a/pycheribuild/projects/project.py +++ b/pycheribuild/projects/project.py @@ -149,6 +149,7 @@ def __init__(self, kind: MakeCommandKind, project: SimpleProject, **kwargs) -> N self.__can_pass_j_flag: "Optional[bool]" = None self.__command: "Optional[str]" = None self.__command_args: "list[str]" = [] + self.exclude_from_csa: "bool" = False def __deepcopy__(self, memo) -> "typing.NoReturn": raise RuntimeError("Should not be called!") @@ -503,6 +504,14 @@ def can_build_with_msan(self) -> bool: def can_build_with_cfi(self) -> bool: return self._xtarget is None or not self._xtarget.is_cheri_purecap() + @classproperty + def can_build_with_csa(self) -> bool: + return False + + @classproperty + def extra_scan_build_args(self) -> "list[str]": + return [] + @classproperty def can_build_with_ccache(self) -> bool: return False @@ -660,11 +669,19 @@ def setup_config_options(cls, install_directory_help="", **kwargs) -> None: ) else: cls.use_asan = False + if cls.can_build_with_msan: cls.use_msan = cls.add_bool_option("use-msan", default=False, help="Build with MemorySanitizer enabled") else: cls.use_msan = False + if cls.can_build_with_csa: + cls.use_csa = cls.add_bool_option( + "use-csa", default=False, help="Build and analyse with Clang Static Analyzer" + ) + else: + cls.use_csa = False + if cls.can_build_with_ccache: cls.use_ccache = cls.add_bool_option("use-ccache", default=False, help="Build with CCache") else: @@ -1209,6 +1226,19 @@ def __setattr__(self, name, value) -> None: ) self.__dict__[name] = value + def _get_scan_build_args(self) -> "list[str]": + scan_build_args = [ + commandline_to_str([self.target_info.scan_build]), + "--keep-cc", + "--use-cc", + commandline_to_str([self.CC]), + "--use-c++", + commandline_to_str([self.CXX]), + ] + if self.extra_scan_build_args: + scan_build_args = scan_build_args + self.extra_scan_build_args + return scan_build_args + def _get_make_commandline( self, make_target: "Optional[Union[str, list[str]]]", @@ -1251,6 +1281,10 @@ def _get_make_commandline( continue_on_error=self.config.pass_dash_k_to_make, ), ] + + if self.use_csa and not options.exclude_from_csa: + all_args = self._get_scan_build_args() + all_args + if not self.config.make_without_nice: all_args = ["nice", *all_args] return all_args @@ -1417,8 +1451,12 @@ def configure(self, *, cwd: "Optional[Path]" = None, configure_path: "Optional[P assert configure_path, "configure_command should not be empty!" if not Path(configure_path).exists(): self.fatal("Configure command ", configure_path, "does not exist!") + + configure_cmd = [str(configure_path), *self.configure_args] + if self.use_csa: + configure_cmd = self._get_scan_build_args() + configure_cmd self.run_with_logfile( - [str(configure_path), *self.configure_args], + configure_cmd, logfile_name="configure", cwd=cwd, env=self.configure_environment, @@ -1576,6 +1614,20 @@ def copy_asan_dependencies(self, dest_libdir) -> None: _check_install_dir_conflict: bool = True + # noinspection PyPep8Naming + def _check_build_settings_for_CSA(self): # noqa: N802 + if self.build_type != BuildType.DEBUG: + self.warning("It's recommended to analyze a project in its Debug configuration.") + if not self.with_clean and len(list(self.build_dir.iterdir())) != 0: + self.warning( + "In order for CSA to analyze the whole project it needs to intercept all compilations." + " Therefore the project must be built from scratch." + " Consider running with --clean." + ) + self.ask_for_confirmation("Are you sure you want to continue?") + if not self.config.skip_install: + self.info("You may want to skip install step when building with CSA.") + def _last_build_kind_path(self) -> Path: return Path(self.build_dir, ".cheribuild_last_build_kind") @@ -1666,6 +1718,8 @@ def process(self) -> None: self.fatal( "Cannot find", libname, "library in compiler dir", expected_path, "-- Compilation will fail!" ) + if self.use_csa: + self._check_build_settings_for_CSA() install_dir_kind = self.get_default_install_dir_kind() if install_dir_kind != DefaultInstallDir.DO_NOT_INSTALL and self._check_install_dir_conflict: xtarget: CrossCompileTarget = self._xtarget @@ -1920,8 +1974,8 @@ def _prepare_toolchain_file_common(self, output_file: "Optional[Path]" = None, * ), TOOLCHAIN_CXX_FLAGS=cmdline(self.CXXFLAGS), TOOLCHAIN_ASM_FLAGS=cmdline(self.ASMFLAGS), - TOOLCHAIN_C_COMPILER=self.CC, - TOOLCHAIN_CXX_COMPILER=self.CXX, + TOOLCHAIN_C_COMPILER=self.cc_wrapper, + TOOLCHAIN_CXX_COMPILER=self.cxx_wrapper, TOOLCHAIN_AR=self.target_info.ar, TOOLCHAIN_RANLIB=self.target_info.ranlib, TOOLCHAIN_NM=self.target_info.nm, @@ -2128,9 +2182,9 @@ def setup(self) -> None: super().setup() # Most projects expect that a plain $CC foo.c will work so we include the -target, etc in CC essential_flags = self.essential_compiler_and_linker_flags - self.set_make_cmd_with_args("CC", self.CC, essential_flags) + self.set_make_cmd_with_args("CC", self.cc_wrapper, essential_flags) self.set_make_cmd_with_args("CPP", self.CPP, essential_flags) - self.set_make_cmd_with_args("CXX", self.CXX, essential_flags) + self.set_make_cmd_with_args("CXX", self.cxx_wrapper, essential_flags) self.set_make_cmd_with_args("CCLD", self.CC, essential_flags) self.set_make_cmd_with_args("CXXLD", self.CXX, essential_flags) self.make_args.set_env(AR=self.target_info.ar) diff --git a/pycheribuild/projects/simple_project.py b/pycheribuild/projects/simple_project.py index a9ee6bfc4..c4a4828f6 100644 --- a/pycheribuild/projects/simple_project.py +++ b/pycheribuild/projects/simple_project.py @@ -398,6 +398,7 @@ class SimpleProject(AbstractProject, metaclass=ABCMeta if typing.TYPE_CHECKING e build_dir_suffix: str = "" # add a suffix to the build dir (e.g. for freebsd-with-bootstrap-clang) use_asan: bool = False use_msan: bool = False + use_csa: bool = False # Analyse with Clang Static Analyzer add_build_dir_suffix_for_native: bool = False # Whether to add -native to the native build dir build_in_source_dir: bool = False # For projects that can't build in the source dir build_via_symlink_farm: bool = False # Create source symlink farm to work around lack of out-of-tree build support @@ -734,11 +735,25 @@ def get_host_triple(self) -> str: def CC(self) -> Path: # noqa: N802 return self.target_info.c_compiler + @property + def cc_wrapper(self) -> Path: + if not self.use_csa: + return self.CC + else: + return self.target_info.ccc_analyzer + # noinspection PyPep8Naming @property def CXX(self) -> Path: # noqa: N802 return self.target_info.cxx_compiler + @property + def cxx_wrapper(self) -> Path: + if not self.use_csa: + return self.CXX + else: + return self.target_info.cxx_analyzer + # noinspection PyPep8Naming @property def CPP(self) -> Path: # noqa: N802 @@ -802,6 +817,8 @@ def build_configuration_suffix(self, target: Optional[CrossCompileTarget] = None result += self.build_dir_suffix if self.use_asan: result += "-asan" + if self.use_csa: + result += "-csa" if self.auto_var_init != AutoVarInit.NONE: result += "-init-" + str(self.auto_var_init.value) # targets that only support native might not need a suffix diff --git a/tests/test_argument_parsing.py b/tests/test_argument_parsing.py index ea269292d..1c66a16e2 100644 --- a/tests/test_argument_parsing.py +++ b/tests/test_argument_parsing.py @@ -1280,6 +1280,7 @@ def test_mfs_root_kernel_config_options(): "mfs_root_image", "skip_update", "use_ccache", + "use_csa", "use_lto", "with_clean", "with_debug_files",