diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbb8abd..27dbe49 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -85,3 +85,183 @@ jobs: run: | set -euo pipefail ./build/ctrace -h + + build-windows: + name: Build on Windows (pinned LLVM 20.1.0) + runs-on: windows-2022 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Resolve dependency ref + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $ref = if ($env:GITHUB_HEAD_REF) { $env:GITHUB_HEAD_REF } else { $env:GITHUB_REF_NAME } + "DEPENDENCY_REF=$ref" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Checkout coretrace-compiler matching ref + id: checkout_compiler_ref + continue-on-error: true + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-compiler + ref: ${{ env.DEPENDENCY_REF }} + path: deps/coretrace-compiler + fetch-depth: 1 + + - name: Checkout coretrace-compiler main fallback + if: steps.checkout_compiler_ref.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-compiler + ref: main + path: deps/coretrace-compiler + fetch-depth: 1 + + - name: Checkout coretrace-stack-analyzer matching ref + id: checkout_stack_ref + continue-on-error: true + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-stack-analyzer + ref: ${{ env.DEPENDENCY_REF }} + path: deps/coretrace-stack-analyzer + fetch-depth: 1 + + - name: Checkout coretrace-stack-analyzer main fallback + if: steps.checkout_stack_ref.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-stack-analyzer + ref: main + path: deps/coretrace-stack-analyzer + fetch-depth: 1 + + - name: Checkout coretrace-log matching ref + id: checkout_logger_ref + continue-on-error: true + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-log + ref: ${{ env.DEPENDENCY_REF }} + path: deps/coretrace-log + fetch-depth: 1 + + - name: Checkout coretrace-log main fallback + if: steps.checkout_logger_ref.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-log + ref: main + path: deps/coretrace-log + fetch-depth: 1 + + - name: Install LLVM 20.1.0 archive with 7-Zip + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + $llvmVersion = "20.1.0" + $archiveName = "clang+llvm-$llvmVersion-x86_64-pc-windows-msvc.tar.xz" + $archiveUrl = "https://github.com/llvm/llvm-project/releases/download/llvmorg-$llvmVersion/$archiveName" + $archivePath = Join-Path $env:RUNNER_TEMP $archiveName + $extractRoot = Join-Path $env:RUNNER_TEMP "llvm-extract" + $installRoot = "C:\Program Files\LLVM-$llvmVersion" + $sevenZip = "${env:ProgramFiles}\7-Zip\7z.exe" + $tarPath = Join-Path $env:RUNNER_TEMP "clang+llvm-$llvmVersion-x86_64-pc-windows-msvc.tar" + + if (-not (Test-Path $sevenZip)) { + throw "7z.exe not found at $sevenZip" + } + + Write-Host "Downloading LLVM archive from $archiveUrl" + Invoke-WebRequest -Uri $archiveUrl -OutFile $archivePath + Write-Host "Download finished" + Get-Item $archivePath | Select-Object FullName, Length | Format-Table -AutoSize + + if (Test-Path $extractRoot) { + Remove-Item -Recurse -Force $extractRoot + } + New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null + + if (Test-Path $tarPath) { + Remove-Item -Force $tarPath + } + + Write-Host "Extracting .xz to .tar with 7-Zip" + & $sevenZip x $archivePath "-o$env:RUNNER_TEMP" -y + if ($LASTEXITCODE -ne 0) { + throw "7-Zip failed while extracting .xz archive" + } + + if (-not (Test-Path $tarPath)) { + throw "Expected tar file not found after xz extraction: $tarPath" + } + + Write-Host "Extracting .tar to $extractRoot" + & $sevenZip x $tarPath "-o$extractRoot" -y + if ($LASTEXITCODE -ne 0) { + throw "7-Zip failed while extracting .tar archive" + } + + Write-Host "Listing extracted directories" + Get-ChildItem $extractRoot -Directory | Select-Object Name, FullName | Format-Table -AutoSize + + $extractedDir = Get-ChildItem $extractRoot -Directory | Select-Object -First 1 + if (-not $extractedDir) { + throw "Failed to find extracted LLVM directory under $extractRoot" + } + + if (Test-Path $installRoot) { + Remove-Item -Recurse -Force $installRoot + } + Move-Item -Path $extractedDir.FullName -Destination $installRoot + + $clangClPath = Join-Path $installRoot "bin\clang-cl.exe" + $llvmCmakeDir = Join-Path $installRoot "lib\cmake\llvm" + $clangCmakeDir = Join-Path $installRoot "lib\cmake\clang" + $llvmConfigPath = Join-Path $llvmCmakeDir "LLVMConfig.cmake" + $clangConfigPath = Join-Path $clangCmakeDir "ClangConfig.cmake" + + Write-Host "Checking extracted LLVM files" + Write-Host "clang-cl path: $clangClPath" + Write-Host "LLVMConfig path: $llvmConfigPath" + Write-Host "ClangConfig path: $clangConfigPath" + + if (-not (Test-Path $clangClPath)) { + throw "clang-cl.exe not found after LLVM archive extraction: $clangClPath" + } + if (-not (Test-Path $llvmConfigPath)) { + throw "LLVMConfig.cmake not found after LLVM archive extraction: $llvmConfigPath" + } + if (-not (Test-Path $clangConfigPath)) { + throw "ClangConfig.cmake not found after LLVM archive extraction: $clangConfigPath" + } + + $clangVersion = (& $clangClPath --version) -join " " + if ($clangVersion -notmatch "clang version 20\.1\.0") { + throw "Unexpected clang-cl version. Got: $clangVersion" + } + + "LLVM_CMAKE_DIR=$llvmCmakeDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "LLVM_DIR=$llvmCmakeDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "Clang_DIR=$clangCmakeDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Configure + Build + Install + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + powershell -ExecutionPolicy Bypass -File .\scripts\build-windows.ps1 ` + -LLVMDir "${env:LLVM_CMAKE_DIR}" ` + -CompilerSourceDir "${PWD}\deps\coretrace-compiler" ` + -StackAnalyzerSourceDir "${PWD}\deps\coretrace-stack-analyzer" ` + -LoggerSourceDir "${PWD}\deps\coretrace-log" ` + -Configuration Release + + - name: Smoke test + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + .\dist\windows\bin\ctrace.exe -h diff --git a/.github/workflows/release-binaries.yml b/.github/workflows/release-binaries.yml index d07a94b..e5fca88 100644 --- a/.github/workflows/release-binaries.yml +++ b/.github/workflows/release-binaries.yml @@ -100,10 +100,219 @@ jobs: dist/${{ matrix.arch }}/*.sha256 if-no-files-found: error + build-windows-binary: + name: Build Windows binary (x64, LLVM 20.1.0) + runs-on: windows-2022 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Resolve dependency ref + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + $ref = if ($env:GITHUB_HEAD_REF) { $env:GITHUB_HEAD_REF } else { $env:GITHUB_REF_NAME } + "DEPENDENCY_REF=$ref" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Checkout coretrace-compiler matching ref + id: checkout_compiler_ref + continue-on-error: true + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-compiler + ref: ${{ env.DEPENDENCY_REF }} + path: deps/coretrace-compiler + fetch-depth: 1 + + - name: Checkout coretrace-compiler main fallback + if: steps.checkout_compiler_ref.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-compiler + ref: main + path: deps/coretrace-compiler + fetch-depth: 1 + + - name: Checkout coretrace-stack-analyzer matching ref + id: checkout_stack_ref + continue-on-error: true + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-stack-analyzer + ref: ${{ env.DEPENDENCY_REF }} + path: deps/coretrace-stack-analyzer + fetch-depth: 1 + + - name: Checkout coretrace-stack-analyzer main fallback + if: steps.checkout_stack_ref.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-stack-analyzer + ref: main + path: deps/coretrace-stack-analyzer + fetch-depth: 1 + + - name: Checkout coretrace-log matching ref + id: checkout_logger_ref + continue-on-error: true + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-log + ref: ${{ env.DEPENDENCY_REF }} + path: deps/coretrace-log + fetch-depth: 1 + + - name: Checkout coretrace-log main fallback + if: steps.checkout_logger_ref.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: CoreTrace/coretrace-log + ref: main + path: deps/coretrace-log + fetch-depth: 1 + + - name: Resolve artifact version + id: version + shell: bash + run: | + set -euo pipefail + short_sha="$(git rev-parse --short=12 HEAD)" + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + version="${GITHUB_REF_NAME}" + else + version="sha-${short_sha}" + fi + echo "value=${version}" >> "${GITHUB_OUTPUT}" + + - name: Install LLVM 20.1.0 archive with 7-Zip + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + + $llvmVersion = "20.1.0" + $archiveName = "clang+llvm-$llvmVersion-x86_64-pc-windows-msvc.tar.xz" + $archiveUrl = "https://github.com/llvm/llvm-project/releases/download/llvmorg-$llvmVersion/$archiveName" + $archivePath = Join-Path $env:RUNNER_TEMP $archiveName + $extractRoot = Join-Path $env:RUNNER_TEMP "llvm-extract" + $installRoot = "C:\Program Files\LLVM-$llvmVersion" + $sevenZip = "${env:ProgramFiles}\7-Zip\7z.exe" + $tarPath = Join-Path $env:RUNNER_TEMP "clang+llvm-$llvmVersion-x86_64-pc-windows-msvc.tar" + + if (-not (Test-Path $sevenZip)) { + throw "7z.exe not found at $sevenZip" + } + + Write-Host "Downloading LLVM archive from $archiveUrl" + Invoke-WebRequest -Uri $archiveUrl -OutFile $archivePath + Write-Host "Download finished" + Get-Item $archivePath | Select-Object FullName, Length | Format-Table -AutoSize + + if (Test-Path $extractRoot) { + Remove-Item -Recurse -Force $extractRoot + } + New-Item -ItemType Directory -Force -Path $extractRoot | Out-Null + + if (Test-Path $tarPath) { + Remove-Item -Force $tarPath + } + + Write-Host "Extracting .xz to .tar with 7-Zip" + & $sevenZip x $archivePath "-o$env:RUNNER_TEMP" -y + if ($LASTEXITCODE -ne 0) { + throw "7-Zip failed while extracting .xz archive" + } + + if (-not (Test-Path $tarPath)) { + throw "Expected tar file not found after xz extraction: $tarPath" + } + + Write-Host "Extracting .tar to $extractRoot" + & $sevenZip x $tarPath "-o$extractRoot" -y + if ($LASTEXITCODE -ne 0) { + throw "7-Zip failed while extracting .tar archive" + } + + Write-Host "Listing extracted directories" + Get-ChildItem $extractRoot -Directory | Select-Object Name, FullName | Format-Table -AutoSize + + $extractedDir = Get-ChildItem $extractRoot -Directory | Select-Object -First 1 + if (-not $extractedDir) { + throw "Failed to find extracted LLVM directory under $extractRoot" + } + + if (Test-Path $installRoot) { + Remove-Item -Recurse -Force $installRoot + } + Move-Item -Path $extractedDir.FullName -Destination $installRoot + + $clangClPath = Join-Path $installRoot "bin\clang-cl.exe" + $llvmCmakeDir = Join-Path $installRoot "lib\cmake\llvm" + $clangCmakeDir = Join-Path $installRoot "lib\cmake\clang" + $llvmConfigPath = Join-Path $llvmCmakeDir "LLVMConfig.cmake" + $clangConfigPath = Join-Path $clangCmakeDir "ClangConfig.cmake" + + Write-Host "Checking extracted LLVM files" + Write-Host "clang-cl path: $clangClPath" + Write-Host "LLVMConfig path: $llvmConfigPath" + Write-Host "ClangConfig path: $clangConfigPath" + + if (-not (Test-Path $clangClPath)) { + throw "clang-cl.exe not found after LLVM archive extraction: $clangClPath" + } + if (-not (Test-Path $llvmConfigPath)) { + throw "LLVMConfig.cmake not found after LLVM archive extraction: $llvmConfigPath" + } + if (-not (Test-Path $clangConfigPath)) { + throw "ClangConfig.cmake not found after LLVM archive extraction: $clangConfigPath" + } + + $clangVersion = (& $clangClPath --version) -join " " + if ($clangVersion -notmatch "clang version 20\.1\.0") { + throw "Unexpected clang-cl version. Got: $clangVersion" + } + + "LLVM_CMAKE_DIR=$llvmCmakeDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "LLVM_DIR=$llvmCmakeDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + "Clang_DIR=$clangCmakeDir" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + + - name: Build packaged binary + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + powershell -ExecutionPolicy Bypass -File .\scripts\build-windows.ps1 ` + -LLVMDir "${env:LLVM_CMAKE_DIR}" ` + -CompilerSourceDir "${PWD}\deps\coretrace-compiler" ` + -StackAnalyzerSourceDir "${PWD}\deps\coretrace-stack-analyzer" ` + -LoggerSourceDir "${PWD}\deps\coretrace-log" ` + -Configuration Release ` + -VersionOverride "${{ steps.version.outputs.value }}" ` + -PackageZip + + - name: Stage release assets + shell: pwsh + run: | + $ErrorActionPreference = "Stop" + New-Item -ItemType Directory -Force -Path dist/windows | Out-Null + $zipFile = "coretrace-windows-Release.zip" + if (-not (Test-Path $zipFile)) { + throw "missing archive: $zipFile" + } + Copy-Item -LiteralPath $zipFile -Destination "dist/windows/${{ env.RELEASE_BASENAME }}-${{ steps.version.outputs.value }}-windows-amd64.zip" -Force + + - name: Upload workflow artifacts + uses: actions/upload-artifact@v4 + with: + name: ${{ env.RELEASE_BASENAME }}-${{ steps.version.outputs.value }}-windows-amd64 + path: dist/windows/*.zip + if-no-files-found: error + publish-release-assets: name: Attach binaries to GitHub Release runs-on: ubuntu-24.04 - needs: build-linux-binaries + needs: + - build-linux-binaries + - build-windows-binary if: startsWith(github.ref, 'refs/tags/v') permissions: contents: write @@ -112,7 +321,7 @@ jobs: - name: Download build artifacts uses: actions/download-artifact@v4 with: - pattern: ${{ env.RELEASE_BASENAME }}-*-linux-* + pattern: ${{ env.RELEASE_BASENAME }}-* path: dist merge-multiple: true @@ -128,5 +337,6 @@ jobs: files: | dist/*.tar.gz dist/*.sha256 + dist/*.zip fail_on_unmatched_files: true generate_release_notes: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d8cce8f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist +build-win \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 35c1def..6e9ec7e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,11 +1,23 @@ # SPDX-License-Identifier: Apache-2.0 cmake_minimum_required(VERSION 3.28) +cmake_policy(SET CMP0091 NEW) project(ArgumentManagerProject VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(MSVC) + set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") +endif() + +function(coretrace_force_msvc_runtime target_name) + if(MSVC AND TARGET "${target_name}") + set_property(TARGET "${target_name}" PROPERTY + MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") + endif() +endfunction() + include(${CMAKE_SOURCE_DIR}/cmake/CoreTraceVersion.cmake) coretrace_resolve_version(CTRACE_VERSION_STRING CTRACE_VERSION_SOURCE) @@ -42,17 +54,21 @@ set(COMMON_SOURCES main.cpp ) -set(SPECIFIC_SOURCES - src/ArgumentParser/GetOpt/GetoptArgumentParser.cpp - src/ArgumentParser/CLI11/CLI11ArgumentParser.cpp -) +if (PARSER_TYPE STREQUAL "Getopt") + if (WIN32) + message(FATAL_ERROR "PARSER_TYPE=Getopt is not supported on Windows. Use CLI11 instead.") + endif() +elseif (PARSER_TYPE STREQUAL "CLI11") +else() + message(FATAL_ERROR "Unsupported PARSER_TYPE: ${PARSER_TYPE}") +endif() set(SOURCES ${COMMON_SOURCES} - ${SPECIFIC_SOURCES} ) add_executable(ctrace ${SOURCES}) +coretrace_force_msvc_runtime(ctrace) if (PARSER_TYPE STREQUAL "Getopt") target_compile_definitions(ctrace PRIVATE USE_GETOPT) @@ -85,6 +101,10 @@ target_link_libraries(ctrace PRIVATE coretrace::logger) include(${CMAKE_SOURCE_DIR}/cmake/httpLib.cmake) target_link_libraries(ctrace PRIVATE httplib::httplib) +if (WIN32) + target_link_libraries(ctrace PRIVATE ws2_32) +endif() + # === OPTIONAL DEPENDENCIES === option(USE_EXTERNAL_CLI11 "Download CLI11" OFF) @@ -174,11 +194,14 @@ add_executable(ctrace_config_tests src/ArgumentParser/BaseArgumentParser.cpp src/ArgumentParser/ArgumentManager.cpp src/ArgumentParser/ArgumentParserFactory.cpp - src/ArgumentParser/CLI11/CLI11ArgumentParser.cpp - src/ArgumentParser/GetOpt/GetoptArgumentParser.cpp src/ctrace_tools/strings.cpp ) +coretrace_force_msvc_runtime(ctrace_config_tests) target_link_libraries(ctrace_config_tests PRIVATE nlohmann_json::nlohmann_json coretrace::logger) +if (WIN32) + target_link_libraries(ctrace_config_tests PRIVATE ws2_32) +endif() + add_test(NAME ctrace_config_tests COMMAND ctrace_config_tests) diff --git a/README.md b/README.md index 4021360..53d7b29 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,22 @@ cmake .. \ make -j4 ``` +### WINDOWS + +CoreTrace can be built as a native `ctrace.exe` on Windows with [`scripts/build-windows.ps1`](scripts/build-windows.ps1). + +Example: + +```powershell +powershell -ExecutionPolicy Bypass -File .\scripts\build-windows.ps1 ` + -LLVMDir "C:\LLVM\lib\cmake\llvm" ` + -CompilerSourceDir "C:\Users\shookapic\Documents\coretrace-compiler" ` + -StackAnalyzerSourceDir "C:\Users\shookapic\Documents\coretrace-stack-analyzer" ` + -LoggerSourceDir "C:\Users\shookapic\Documents\coretrace-log" ` + -PackageZip +``` +! Change **-LLVMDIR** according to your LLVM installation path ! + ### CODE STYLE (clang-format) - Version cible : `clang-format` 17 (utilisée dans la CI). @@ -80,8 +96,8 @@ Options: --invoke Invokes specific tools (comma-separated). Available tools: flawfinder, ikos, cppcheck, tscancode, ctrace_stack_analyzer. --input Specifies the source files to analyse (comma-separated). - --ipc Specifies the IPC method to use (e.g., fifo, socket). - --ipc-path Specifies the IPC path (default: /tmp/coretrace_ipc). + --ipc Specifies the IPC method to use (e.g., socket). + --ipc-path Specifies the IPC path (platform-specific default). --serve-host HTTP server host when --ipc=serve. --serve-port HTTP server port when --ipc=serve. --shutdown-token Token required for POST /shutdown (server mode). @@ -154,7 +170,7 @@ curl -X POST http://127.0.0.1:8080/api \ "report_file": "ctrace-report.txt", "output_file": "ctrace.out", "ipc": "serve", - "ipc_path": "/tmp/coretrace_ipc", + "ipc_path": "", "async": false, "verbose": true } diff --git a/cmake/logger/coretraceLog.cmake b/cmake/logger/coretraceLog.cmake index 3fb6da6..9e21c1d 100644 --- a/cmake/logger/coretraceLog.cmake +++ b/cmake/logger/coretraceLog.cmake @@ -4,9 +4,17 @@ set(CORETRACE_LOGGER_BUILD_TESTS OFF CACHE BOOL "Disable logger tests" FORCE) include(FetchContent) -FetchContent_Declare(coretrace-logger +FetchContent_Declare(coretrace_logger GIT_REPOSITORY https://github.com/CoreTrace/coretrace-log.git GIT_TAG main EXCLUDE_FROM_ALL ) -FetchContent_MakeAvailable(coretrace-logger) + +FetchContent_GetProperties(coretrace_logger) +if(NOT coretrace_logger_POPULATED) + FetchContent_Populate(coretrace_logger) +endif() + +if(NOT TARGET coretrace_logger) + FetchContent_MakeAvailable(coretrace_logger) +endif() diff --git a/cmake/stackUsageAnalyzer.cmake b/cmake/stackUsageAnalyzer.cmake index 6ee7cf5..41ac1b0 100644 --- a/cmake/stackUsageAnalyzer.cmake +++ b/cmake/stackUsageAnalyzer.cmake @@ -1,19 +1,29 @@ # SPDX-License-Identifier: Apache-2.0 include(FetchContent) +set(CORETRACE_COMPILER_GIT_TAG "main" CACHE STRING + "Git ref used when fetching coretrace-compiler") +set(CORETRACE_STACK_ANALYZER_GIT_TAG "main" CACHE STRING + "Git ref used when fetching coretrace-stack-analyzer") + +FetchContent_Declare( + cc + GIT_REPOSITORY https://github.com/CoreTrace/coretrace-compiler.git + GIT_TAG ${CORETRACE_COMPILER_GIT_TAG} + EXCLUDE_FROM_ALL +) +FetchContent_MakeAvailable(cc) + FetchContent_Declare( stack_analyzer GIT_REPOSITORY https://github.com/CoreTrace/coretrace-stack-analyzer.git - GIT_TAG v0.18.1 + GIT_TAG ${CORETRACE_STACK_ANALYZER_GIT_TAG} EXCLUDE_FROM_ALL ) - FetchContent_MakeAvailable(stack_analyzer) # Copy upstream default models into config/models/ so that tool-config.json # can reference them with paths relative to the config directory, without # ever pointing into _deps/. -# Custom model files already present in config/models/ are not affected -# (they live at the root of the directory, upstream models are in subdirs). file(COPY "${stack_analyzer_SOURCE_DIR}/models/" DESTINATION "${CMAKE_SOURCE_DIR}/config/models") diff --git a/config/tool-config.json b/config/tool-config.json index 2a29a36..566426d 100644 --- a/config/tool-config.json +++ b/config/tool-config.json @@ -25,7 +25,7 @@ "runtime": { "async": false, "ipc": "standardIO", - "ipc_path": "/tmp/coretrace_ipc" + "ipc_path": "" }, "server": { "host": "127.0.0.1", diff --git a/docs/configuration.md b/docs/configuration.md index 7ef9c6f..aee1e5b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -149,7 +149,7 @@ CLI: `--ipc` - `runtime.ipc_path` Type: `string` -Default: `"/tmp/coretrace_ipc"` +Default: platform-specific temp socket path when omitted or empty Allowed: socket path. Description: IPC socket path. Impact: used when IPC mode is socket. diff --git a/include/App/Platform.hpp b/include/App/Platform.hpp new file mode 100644 index 0000000..7985236 --- /dev/null +++ b/include/App/Platform.hpp @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef APP_PLATFORM_HPP +#define APP_PLATFORM_HPP + +#include +#include +#include +#include + +namespace ctrace::platform +{ + [[nodiscard]] inline bool isWindows() noexcept + { +#ifdef _WIN32 + return true; +#else + return false; +#endif + } + + [[nodiscard]] inline std::string defaultIpcPath() + { +#ifdef _WIN32 + const char* tempDir = std::getenv("TEMP"); + std::filesystem::path baseDir; + if (tempDir != nullptr && *tempDir != '\0') + { + baseDir = tempDir; + } + else + { + try + { + baseDir = std::filesystem::temp_directory_path(); + } + catch (const std::exception&) + { + baseDir = "."; + } + } + return (baseDir / "coretrace_ipc.sock").string(); +#else + return "/tmp/coretrace_ipc.sock"; +#endif + } +} // namespace ctrace::platform + +#endif // APP_PLATFORM_HPP diff --git a/include/App/ToolResolver.hpp b/include/App/ToolResolver.hpp new file mode 100644 index 0000000..1536444 --- /dev/null +++ b/include/App/ToolResolver.hpp @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef APP_TOOL_RESOLVER_HPP +#define APP_TOOL_RESOLVER_HPP + +#include +#include +#include +#include +#include + +namespace ctrace +{ + struct ResolvedToolCommand + { + std::string executable; + std::vector prefixArguments; + }; + + namespace detail + { + [[nodiscard]] inline bool hasEnvValue(const char* name) + { + const char* value = std::getenv(name); + return value != nullptr && *value != '\0'; + } + + [[nodiscard]] inline std::string envOrDefault(const char* name, std::string_view fallback) + { + const char* value = std::getenv(name); + if (value == nullptr || *value == '\0') + { + return std::string(fallback); + } + return value; + } + + [[nodiscard]] inline std::string repoLocalExecutable(std::string_view relativeStem) + { + std::filesystem::path path(relativeStem); +#ifdef _WIN32 + path += ".exe"; +#endif + return path.make_preferred().string(); + } + } // namespace detail + + [[nodiscard]] inline ResolvedToolCommand resolveCppcheckCommand() + { + return {detail::envOrDefault("CORETRACE_CPPCHECK_BIN", "cppcheck"), {}}; + } + + [[nodiscard]] inline ResolvedToolCommand resolveTscancodeCommand() + { +#ifdef _WIN32 + constexpr std::string_view fallback = "tscancode"; +#else + const std::string fallback = + detail::repoLocalExecutable("./tscancode/src/tscancode/trunk/tscancode"); +#endif + return {detail::envOrDefault("CORETRACE_TSCANCODE_BIN", fallback), {}}; + } + + [[nodiscard]] inline ResolvedToolCommand resolveIkosCommand() + { +#ifdef _WIN32 + constexpr std::string_view fallback = "ikos"; +#else + const std::string fallback = detail::repoLocalExecutable("./ikos/src/ikos-build/bin/ikos"); +#endif + return {detail::envOrDefault("CORETRACE_IKOS_BIN", fallback), {}}; + } + + [[nodiscard]] inline ResolvedToolCommand resolveFlawfinderCommand() + { + const bool hasCustomLauncher = detail::hasEnvValue("CORETRACE_FLAWFINDER_LAUNCHER"); +#ifdef _WIN32 + const std::string pythonExe = detail::envOrDefault("CORETRACE_PYTHON_BIN", "python"); +#else + const std::string pythonExe = detail::envOrDefault("CORETRACE_PYTHON_BIN", "python3"); +#endif + + ResolvedToolCommand command{ + detail::envOrDefault("CORETRACE_FLAWFINDER_LAUNCHER", pythonExe), {}}; + + const std::string scriptPath = detail::envOrDefault( + "CORETRACE_FLAWFINDER_SCRIPT", "./flawfinder/src/flawfinder-build/flawfinder.py"); + if (!hasCustomLauncher) + { + command.prefixArguments.push_back(scriptPath); + } + return command; + } +} // namespace ctrace + +#endif // APP_TOOL_RESOLVER_HPP diff --git a/include/Config/config.hpp b/include/Config/config.hpp index b95e7d0..dab347d 100644 --- a/include/Config/config.hpp +++ b/include/Config/config.hpp @@ -13,6 +13,7 @@ #include #include "ArgumentParser/ArgumentManager.hpp" #include "ArgumentParser/ArgumentParserFactory.hpp" +#include "App/Platform.hpp" #include "App/SupportedTools.hpp" #include "App/Version.hpp" #include "ctrace_tools/strings.hpp" @@ -22,57 +23,61 @@ static void printHelp(void) { - std::cout << R"(ctrace - Static & Dynamic C/C++ Code Analysis Tool - -Usage: - ctrace [options] - -Options: - --help Displays this help message. - --version Displays build version information. - --verbose Enables detailed (verbose) output. - --sarif-format Generates a report in SARIF format. - --report-file Specifies the path to the report file (default: ctrace-report.txt). - --output-file Specifies the output file for the analysed binary (default: ctrace.out). - --entry-points Sets the entry points for analysis (default: main). Accepts a comma-separated list. - --config Loads settings from a JSON config file. - --compile-commands Path to compile_commands.json for tools that support it. - --include-compdb-deps Includes dependency entries (e.g. _deps) when auto-loading files from compile_commands.json. - --analysis-profile

Stack analyzer profile: fast|full. - --smt Enables/disables SMT refinement in stack analyzer. - --smt-backend Primary SMT backend (e.g. z3, interval). - --smt-secondary-backend Secondary backend for multi-solver modes. - --smt-mode SMT mode: single|portfolio|cross-check|dual-consensus. - --smt-timeout-ms SMT timeout in milliseconds. - --smt-budget-nodes SMT node budget per query. - --smt-rules Comma-separated SMT-enabled rules. - --resource-model Path to the resource lifetime model for stack analyzer. - --escape-model Path to the stack escape model for stack analyzer. - --buffer-model Path to the buffer overflow model for stack analyzer. - --demangle Displays demangled function names in supported tools. - --static Enables static analysis. - --dyn Enables dynamic analysis. - --invoke Invokes specific tools (comma-separated). - Available tools: flawfinder, ikos, cppcheck, tscancode, ctrace_stack_analyzer. - --input Specifies the source files to analyse (comma-separated). - --timing Enables stack analyzer timing output. - --ipc Specifies the IPC method to use (e.g., fifo, socket). - --ipc-path Specifies the IPC path (default: /tmp/coretrace_ipc). - --serve-host HTTP server host when --ipc=serve. - --serve-port HTTP server port when --ipc=serve. - --async Enables asynchronous execution. - --shutdown-token Token required for POST /shutdown (server mode). - --shutdown-timeout-ms Graceful shutdown timeout in ms (0 = wait indefinitely). - -Examples: - ctrace --input main.cpp,util.cpp --static --invoke=cppcheck,flawfinder - ctrace --verbose --report-file=analysis.txt --sarif-format - -Description: - ctrace is a modular C/C++ code analysis tool that combines both static and dynamic - analysis. It can be finely configured to detect vulnerabilities, security issues, - and memory misuse. -)" << std::endl; + std::cout + << "ctrace - Static & Dynamic C/C++ Code Analysis Tool\n\n" + << "Usage:\n" + << " ctrace [options]\n\n" + << "Options:\n" + << " --help Displays this help message.\n" + << " --version Displays build version information.\n" + << " --verbose Enables detailed (verbose) output.\n" + << " --sarif-format Generates a report in SARIF format.\n" + << " --report-file Specifies the path to the report file (default: " + "ctrace-report.txt).\n" + << " --output-file Specifies the output file for the analysed binary (default: " + "ctrace.out).\n" + << " --entry-points Sets the entry points for analysis (default: main). Accepts " + "a comma-separated list.\n" + << " --config Loads settings from a JSON config file.\n" + << " --compile-commands Path to compile_commands.json for tools that support it.\n" + << " --include-compdb-deps Includes dependency entries (e.g. _deps) when auto-loading " + "files from compile_commands.json.\n" + << " --analysis-profile

Stack analyzer profile: fast|full.\n" + << " --smt Enables/disables SMT refinement in stack analyzer.\n" + << " --smt-backend Primary SMT backend (e.g. z3, interval).\n" + << " --smt-secondary-backend Secondary backend for multi-solver modes.\n" + << " --smt-mode SMT mode: single|portfolio|cross-check|dual-consensus.\n" + << " --smt-timeout-ms SMT timeout in milliseconds.\n" + << " --smt-budget-nodes SMT node budget per query.\n" + << " --smt-rules Comma-separated SMT-enabled rules.\n" + << " --resource-model Path to the resource lifetime model for stack analyzer.\n" + << " --escape-model Path to the stack escape model for stack analyzer.\n" + << " --buffer-model Path to the buffer overflow model for stack analyzer.\n" + << " --demangle Displays demangled function names in supported tools.\n" + << " --static Enables static analysis.\n" + << " --dyn Enables dynamic analysis.\n" + << " --invoke Invokes specific tools (comma-separated).\n" + << " Available tools: flawfinder, ikos, cppcheck, tscancode, " + "ctrace_stack_analyzer.\n" + << " --input Specifies the source files to analyse (comma-separated).\n" + << " --timing Enables stack analyzer timing output.\n" + << " --ipc Specifies the IPC method to use (e.g., socket).\n" + << " --ipc-path Specifies the IPC path (default: " + << ctrace::platform::defaultIpcPath() << ").\n" + << " --serve-host HTTP server host when --ipc=serve.\n" + << " --serve-port HTTP server port when --ipc=serve.\n" + << " --async Enables asynchronous execution.\n" + << " --shutdown-token Token required for POST /shutdown (server mode).\n" + << " --shutdown-timeout-ms Graceful shutdown timeout in ms (0 = wait " + "indefinitely).\n\n" + << "Examples:\n" + << " ctrace --input main.cpp,util.cpp --static --invoke=cppcheck,flawfinder\n" + << " ctrace --verbose --report-file=analysis.txt --sarif-format\n\n" + << "Description:\n" + << " ctrace is a modular C/C++ code analysis tool that combines both static and dynamic\n" + << " analysis. It can be finely configured to detect vulnerabilities, security issues,\n" + << " and memory misuse.\n" + << std::endl; } static void printVersion(void) @@ -120,11 +125,11 @@ namespace ctrace bool hasDynamicAnalysis = false; ///< Indicates if dynamic analysis is enabled. bool hasInvokedSpecificTools = false; ///< Indicates if specific tools are invoked. std::string ipc = - ctrace_defs::IPC_TYPES.front(); ///< IPC method to use (e.g., fifo, socket). - std::string ipcPath = "/tmp/coretrace_ipc"; ///< Path for IPC communication. - std::string serverHost = "127.0.0.1"; ///< Host for server IPC (if applicable). - int serverPort = 8080; ///< Port for server IPC (if applicable). - std::string shutdownToken; ///< Token required for POST /shutdown. + ctrace_defs::IPC_TYPES.front(); ///< IPC method to use (e.g., fifo, socket). + std::string ipcPath = ctrace::platform::defaultIpcPath(); ///< Path for IPC communication. + std::string serverHost = "127.0.0.1"; ///< Host for server IPC (if applicable). + int serverPort = 8080; ///< Port for server IPC (if applicable). + std::string shutdownToken; ///< Token required for POST /shutdown. int shutdownTimeoutMs = 0; ///< Shutdown timeout in milliseconds (0 = wait indefinitely). std::vector specificTools; ///< List of specific tools to invoke. diff --git a/include/Process/Ipc/IpcStrategy.hpp b/include/Process/Ipc/IpcStrategy.hpp index f3d9d24..15dea57 100644 --- a/include/Process/Ipc/IpcStrategy.hpp +++ b/include/Process/Ipc/IpcStrategy.hpp @@ -2,24 +2,29 @@ #ifndef IPC_STRATEGY_HPP #define IPC_STRATEGY_HPP -#include +#include +#include +#include +#include +#include -#include "../ProcessFactory.hpp" -#include "../ThreadProcess.hpp" - -#include -#include -#include +#ifdef _WIN32 +#define NOMINMAX +#include +#include +#include +#else #include #include -#include +#include +#endif class IpcStrategy { public: virtual ~IpcStrategy() = default; virtual void write(const std::string& data) = 0; - virtual void close() = 0; // Pour cleanup RAII + virtual void close() = 0; }; namespace ctrace::ipc @@ -32,56 +37,160 @@ namespace ctrace::ipc { } }; + +#ifdef _WIN32 + class WinsockSession + { + public: + WinsockSession() + { + WSADATA wsaData{}; + const int result = WSAStartup(MAKEWORD(2, 2), &wsaData); + if (result != 0) + { + throw SocketError("WSAStartup failed: " + std::to_string(result)); + } + } + + ~WinsockSession() + { + WSACleanup(); + } + }; + + [[nodiscard]] inline const WinsockSession& winsockSession() + { + static const WinsockSession session; + return session; + } + + [[nodiscard]] inline std::string lastSocketError() + { + return "Winsock error " + std::to_string(WSAGetLastError()); + } +#else + [[nodiscard]] inline std::string lastSocketError() + { + return std::strerror(errno); + } +#endif } // namespace ctrace::ipc -class UnixSocketStrategy : public IpcStrategy +class LocalSocketStrategy : public IpcStrategy { - private: - int sock; - std::string path; - public: - UnixSocketStrategy(const std::string& socketPath) : path(socketPath), sock(-1) + explicit LocalSocketStrategy(const std::string& socketPath) : path_(socketPath) { - sock = socket(AF_UNIX, SOCK_STREAM, 0); - if (sock == -1) +#ifdef _WIN32 + (void)ctrace::ipc::winsockSession(); + socket_ = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_ == INVALID_SOCKET) + { + throw ctrace::ipc::SocketError("Error creating AF_UNIX socket: " + + ctrace::ipc::lastSocketError()); + } +#else + socket_ = ::socket(AF_UNIX, SOCK_STREAM, 0); + if (socket_ == -1) { - throw std::runtime_error("Error socket creation: " + std::string(strerror(errno))); + throw ctrace::ipc::SocketError("Error creating AF_UNIX socket: " + + ctrace::ipc::lastSocketError()); } - sockaddr_un addr; - memset(&addr, 0, sizeof(addr)); +#endif + + sockaddr_un addr{}; addr.sun_family = AF_UNIX; - strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1); - if (connect(sock, (sockaddr*)&addr, sizeof(addr)) == -1) + if (path_.size() >= sizeof(addr.sun_path)) { - throw std::runtime_error("Error socket connexion: " + std::string(strerror(errno))); + close(); + throw ctrace::ipc::SocketError("Socket path is too long: " + path_); + } + std::memcpy(addr.sun_path, path_.c_str(), path_.size()); + addr.sun_path[path_.size()] = '\0'; + + const int connectResult = + ::connect(socket_, reinterpret_cast(&addr), sizeof(addr)); +#ifdef _WIN32 + if (connectResult == SOCKET_ERROR) +#else + if (connectResult == -1) +#endif + { + const std::string error = ctrace::ipc::lastSocketError(); + close(); + throw ctrace::ipc::SocketError("Error connecting to socket '" + path_ + "': " + error); } } - ~UnixSocketStrategy() + + ~LocalSocketStrategy() override { close(); } + void write(const std::string& data) override { - if (sock == -1) + if (data.empty()) { - throw ctrace::ipc::SocketError(std::string("Socket is not connected: ") + - strerror(errno)); + return; } - if (send(sock, data.c_str(), data.size(), 0) == -1) + + if (!isOpen()) { - throw ctrace::ipc::SocketError(std::string("Connection failed: ") + strerror(errno)); + throw ctrace::ipc::SocketError("Socket is not connected"); + } + + std::size_t offset = 0; + while (offset < data.size()) + { +#ifdef _WIN32 + const int sent = + ::send(socket_, data.c_str() + offset, static_cast(data.size() - offset), 0); + if (sent == SOCKET_ERROR) +#else + const auto sent = ::send(socket_, data.c_str() + offset, data.size() - offset, 0); + if (sent == -1) +#endif + { + throw ctrace::ipc::SocketError("Failed to send IPC payload: " + + ctrace::ipc::lastSocketError()); + } + offset += static_cast(sent); } } + void close() override { - if (sock != -1) +#ifdef _WIN32 + if (socket_ != INVALID_SOCKET) + { + ::closesocket(socket_); + socket_ = INVALID_SOCKET; + } +#else + if (socket_ != -1) { - ::close(sock); - sock = -1; - // unlink(path.c_str()); // Optionnel + ::close(socket_); + socket_ = -1; } +#endif } + + private: + [[nodiscard]] bool isOpen() const + { +#ifdef _WIN32 + return socket_ != INVALID_SOCKET; +#else + return socket_ != -1; +#endif + } + + std::string path_; +#ifdef _WIN32 + SOCKET socket_ = INVALID_SOCKET; +#else + int socket_ = -1; +#endif }; #endif // IPC_STRATEGY_HPP diff --git a/include/Process/Process.hpp b/include/Process/Process.hpp index d0e4359..97113f2 100644 --- a/include/Process/Process.hpp +++ b/include/Process/Process.hpp @@ -5,6 +5,7 @@ #include #include #include +#include class Process { diff --git a/include/Process/ProcessFactory.hpp b/include/Process/ProcessFactory.hpp index 19f7489..5ea33d4 100644 --- a/include/Process/ProcessFactory.hpp +++ b/include/Process/ProcessFactory.hpp @@ -4,10 +4,16 @@ #include #include +#include #include "Process.hpp" +#include "ThreadProcess.hpp" + +#if defined(_WIN32) +#include "WinProcess.hpp" +#elif defined(__linux__) || defined(__APPLE__) #include "LinuxProcess.hpp" #include "UnixProcess.hpp" -#include "WinProcess.hpp" +#endif /** * @brief Factory class for creating platform-specific `Process` objects. @@ -46,6 +52,10 @@ class ProcessFactory ctrace::Thread::Output::cout("Creating macOS process for command: " + command); // return std::make_unique(command, args); // doesn't work with tscancode return std::make_unique(command, args); +#else + static_cast(command); + static_cast(args); + throw std::runtime_error("Unsupported platform for process execution"); #endif } }; diff --git a/include/Process/Tools/AnalysisTools.hpp b/include/Process/Tools/AnalysisTools.hpp index 32149f9..d9abfae 100644 --- a/include/Process/Tools/AnalysisTools.hpp +++ b/include/Process/Tools/AnalysisTools.hpp @@ -6,6 +6,7 @@ // #include "IAnalysisTools.hpp" #include "AnalysisToolsBase.hpp" +#include "App/ToolResolver.hpp" #include "ctrace_tools/languageType.hpp" #include "ctrace_tools/mangle.hpp" #include "../ProcessFactory.hpp" @@ -132,8 +133,10 @@ namespace ctrace argsProcess.push_back("--report-file=" + report_file); argsProcess.push_back(src_file); - auto process = ProcessFactory::createProcess( - "./ikos/src/ikos-build/bin/ikos", argsProcess); // ou "cmd.exe" pour Windows + const auto command = ctrace::resolveIkosCommand(); + argsProcess.insert(argsProcess.begin(), command.prefixArguments.begin(), + command.prefixArguments.end()); + auto process = ProcessFactory::createProcess(command.executable, argsProcess); // std::this_thread::sleep_for(std::chrono::seconds(5)); process->execute(); ctrace::Thread::Output::tool_out(process->logOutput); @@ -182,7 +185,6 @@ namespace ctrace { std::vector argsProcess; // = {"flawfinder.py", "-F", "-c", "-C", "-D", "main.c"}; - argsProcess.push_back("./flawfinder/src/flawfinder-build/flawfinder.py"); // argsProcess.push_back("-F"); argsProcess.push_back("-c"); argsProcess.push_back("-C"); @@ -192,8 +194,10 @@ namespace ctrace argsProcess.push_back("--sarif"); } argsProcess.push_back(src_file); - auto process = ProcessFactory::createProcess( - "python3", argsProcess); // or "cmd.exe" for Windows + const auto command = ctrace::resolveFlawfinderCommand(); + argsProcess.insert(argsProcess.begin(), command.prefixArguments.begin(), + command.prefixArguments.end()); + auto process = ProcessFactory::createProcess(command.executable, argsProcess); process->execute(); if (config.global.ipc == "standardIO") @@ -234,7 +238,7 @@ namespace ctrace public: void execute(const std::string& file, ctrace::ProgramConfig config) const override { - ctrace::Thread::Output::cout("Running ikos on " + file); + ctrace::Thread::Output::cout("Running cppcheck on " + file); bool has_sarif_format = config.global.hasSarifFormat; std::string src_file = file; std::string entry_points = config.global.entry_points; @@ -250,8 +254,10 @@ namespace ctrace // argsProcess.push_back("--enable=all"); argsProcess.push_back(src_file); - auto process = ProcessFactory::createProcess( - "/opt/homebrew/bin/cppcheck", argsProcess); // ou "cmd.exe" pour Windows + const auto command = ctrace::resolveCppcheckCommand(); + argsProcess.insert(argsProcess.begin(), command.prefixArguments.begin(), + command.prefixArguments.end()); + auto process = ProcessFactory::createProcess(command.executable, argsProcess); process->execute(); ctrace::Thread::Output::tool_out(process->logOutput); } @@ -263,7 +269,7 @@ namespace ctrace } std::string name() const override { - return "ikos"; + return "cppcheck"; } }; diff --git a/include/Process/Tools/ToolsInvoker.hpp b/include/Process/Tools/ToolsInvoker.hpp index 75c4551..f4be4d2 100644 --- a/include/Process/Tools/ToolsInvoker.hpp +++ b/include/Process/Tools/ToolsInvoker.hpp @@ -136,15 +136,19 @@ namespace ctrace m_ipc = nullptr; // Use std::cout directely coretrace::log(coretrace::Level::Debug, "Using standardIO for IPC.\n"); } - else + else if (m_config.global.ipc == "socket") { - m_ipc = std::make_shared(m_config.global.ipcPath); + m_ipc = std::make_shared(m_config.global.ipcPath); for (auto& [_, tool] : tools) { tool->setIpcStrategy(m_ipc); } } + else + { + m_ipc = nullptr; + } if (m_policy == std::launch::async) { diff --git a/include/Process/WinProcess.hpp b/include/Process/WinProcess.hpp index 848454a..a34b16d 100644 --- a/include/Process/WinProcess.hpp +++ b/include/Process/WinProcess.hpp @@ -3,102 +3,204 @@ #ifdef _WIN32 -#include +#include "Process.hpp" + +#define NOMINMAX #include + #include +#include +#include +#include class WindowsProcess : public Process { public: - WindowsProcess(const std::string& command) : command(command) {} + WindowsProcess(std::string command, std::vector args) + : m_command(std::move(command)), m_arguments_extra(std::move(args)) + { + } protected: void prepare() override { - std::cout << "Preparing Windows process\n"; prepareArguments(); } void run() override { - std::cout << "Running Windows process\n"; + SECURITY_ATTRIBUTES sa{}; + sa.nLength = sizeof(sa); + sa.bInheritHandle = TRUE; + + HANDLE stdoutRead = nullptr; + HANDLE stdoutWrite = nullptr; - // Préparer la commande avec les arguments - std::string cmdLine = command; - for (const auto& arg : arguments) + if (!CreatePipe(&stdoutRead, &stdoutWrite, &sa, 0)) { - cmdLine += " " + arg; + throw std::runtime_error("Failed to create stdout pipe"); } - - STARTUPINFO si = {sizeof(si)}; - PROCESS_INFORMATION pi; - SECURITY_ATTRIBUTES sa = {sizeof(sa), NULL, TRUE}; - - // Créer un fichier pour capturer les logs - HANDLE logFile = CreateFile("process.log", GENERIC_WRITE, FILE_SHARE_READ, &sa, - CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL); - if (logFile == INVALID_HANDLE_VALUE) + if (!SetHandleInformation(stdoutRead, HANDLE_FLAG_INHERIT, 0)) { - throw std::runtime_error("Failed to create log file"); + CloseHandle(stdoutRead); + CloseHandle(stdoutWrite); + throw std::runtime_error("Failed to configure stdout pipe"); } - si.hStdOutput = logFile; - si.hStdError = logFile; - si.dwFlags |= STARTF_USESTDHANDLES; + STARTUPINFOA si{}; + si.cb = sizeof(si); + si.dwFlags = STARTF_USESTDHANDLES; + si.hStdOutput = stdoutWrite; + si.hStdError = stdoutWrite; + si.hStdInput = GetStdHandle(STD_INPUT_HANDLE); + + PROCESS_INFORMATION pi{}; + std::string commandLine = buildCommandLine(); + std::vector mutableCommandLine(commandLine.begin(), commandLine.end()); + mutableCommandLine.push_back('\0'); - // Lancer le processus - if (!CreateProcess(NULL, const_cast(cmdLine.c_str()), NULL, NULL, TRUE, 0, NULL, - NULL, &si, &pi)) + const BOOL created = CreateProcessA(nullptr, mutableCommandLine.data(), nullptr, nullptr, + TRUE, CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi); + + CloseHandle(stdoutWrite); + if (!created) { - CloseHandle(logFile); - throw std::runtime_error("Failed to create process"); + CloseHandle(stdoutRead); + throw std::runtime_error("Failed to create process: " + lastErrorMessage()); } - // Attendre que le processus se termine WaitForSingleObject(pi.hProcess, INFINITE); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); - CloseHandle(logFile); + m_stdoutRead = stdoutRead; captureLogs(); } void cleanup() override { - std::cout << "Cleaning up Windows process\n"; - DeleteFile("process.log"); // Supprimer le fichier de logs temporaire + if (m_stdoutRead != nullptr) + { + CloseHandle(m_stdoutRead); + m_stdoutRead = nullptr; + } } void prepareArguments() override { - std::cout << "Preparing arguments for Windows\n"; - arguments.clear(); - arguments.push_back("/C"); // Exemple pour `cmd.exe` - arguments.push_back("dir"); // Commande `dir` pour lister les fichiers + m_arguments = m_arguments_extra; } void captureLogs() override { - std::cout << "Capturing logs for Windows\n"; - FILE* logFile = fopen("process.log", "r"); - if (!logFile) + if (m_stdoutRead == nullptr) { - throw std::runtime_error("Failed to open log file"); + return; } - char buffer[1024]; logOutput.clear(); - while (fgets(buffer, sizeof(buffer), logFile)) + char buffer[4096]; + DWORD bytesRead = 0; + while (ReadFile(m_stdoutRead, buffer, sizeof(buffer), &bytesRead, nullptr) && bytesRead > 0) { - logOutput += buffer; + logOutput.append(buffer, buffer + bytesRead); } - fclose(logFile); - - std::cout << "Logs captured: " << logOutput << "\n"; } private: - std::string command; + [[nodiscard]] static std::string quoteArgument(const std::string& argument) + { + if (argument.empty()) + { + return "\"\""; + } + + bool needsQuotes = false; + for (const char ch : argument) + { + if (ch == ' ' || ch == '\t' || ch == '"') + { + needsQuotes = true; + break; + } + } + if (!needsQuotes) + { + return argument; + } + + std::string quoted = "\""; + std::size_t backslashCount = 0; + for (const char ch : argument) + { + if (ch == '\\') + { + ++backslashCount; + continue; + } + if (ch == '"') + { + quoted.append(backslashCount * 2 + 1, '\\'); + quoted.push_back('"'); + backslashCount = 0; + continue; + } + + if (backslashCount > 0) + { + quoted.append(backslashCount, '\\'); + backslashCount = 0; + } + quoted.push_back(ch); + } + + if (backslashCount > 0) + { + quoted.append(backslashCount * 2, '\\'); + } + quoted.push_back('"'); + return quoted; + } + + [[nodiscard]] static std::string lastErrorMessage() + { + const DWORD error = GetLastError(); + LPSTR messageBuffer = nullptr; + const DWORD size = + FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, error, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + reinterpret_cast(&messageBuffer), 0, nullptr); + + std::string message = (size > 0 && messageBuffer != nullptr) + ? std::string(messageBuffer, size) + : ("Windows error " + std::to_string(error)); + if (messageBuffer != nullptr) + { + LocalFree(messageBuffer); + } + while (!message.empty() && + (message.back() == '\r' || message.back() == '\n' || message.back() == ' ')) + { + message.pop_back(); + } + return message; + } + + [[nodiscard]] std::string buildCommandLine() const + { + std::string commandLine = quoteArgument(m_command); + for (const auto& argument : m_arguments) + { + commandLine.push_back(' '); + commandLine += quoteArgument(argument); + } + return commandLine; + } + + std::string m_command; + std::vector m_arguments_extra; + HANDLE m_stdoutRead = nullptr; }; #endif diff --git a/include/ctrace_tools/mangle.hpp b/include/ctrace_tools/mangle.hpp index e0480e9..a07e25a 100644 --- a/include/ctrace_tools/mangle.hpp +++ b/include/ctrace_tools/mangle.hpp @@ -1,10 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 +#pragma once + +#include +#include #include +#include #include -#include -#include -#include -#include + +#include namespace ctrace_tools::mangle { @@ -15,39 +18,28 @@ namespace ctrace_tools::mangle * requiring it can be implicitly converted to `std::string_view`. */ template concept StringLike = std::convertible_to; - // TODO: add mangling for windows - /** * @brief Checks if a given name is a mangled C++ symbol. * - * This function determines whether a given name follows the Itanium C++ ABI - * mangling conventions (e.g., names starting with `_Z`). + * This function uses LLVM's cross-platform demangler so it can recognize + * both Itanium-style names (`_Z...`) and Microsoft-style names (`?...`). * * @tparam T A type satisfying the `StringLike` concept. * @param name The name to check for mangling. * @return `true` if the name is mangled, `false` otherwise. - * - * @note This function uses `abi::__cxa_demangle` to attempt demangling. - * If the demangling succeeds, the name is considered mangled. - * @note This implementation is specific to platforms using the Itanium C++ ABI - * (e.g., Linux, macOS). Windows mangling is not yet supported. - * @note The function is marked `[[nodiscard]]`, meaning the return value - * should not be ignored. It is also `noexcept`, indicating that it - * does not throw exceptions. */ [[nodiscard]] bool isMangled(StringLike auto name) noexcept { - int status = 0; - std::string_view sv{name}; + const std::string_view sv{name}; - if (sv.length() < 2 || sv.substr(0, 2) != "_Z") + const bool looksMangled = + sv.starts_with("_Z") || sv.starts_with("?") || sv.starts_with(".?"); + if (!looksMangled) { return false; } - std::unique_ptr demangled( - abi::__cxa_demangle(sv.data(), nullptr, nullptr, &status), std::free); - return status == 0; + return llvm::demangle(sv) != sv; } /** diff --git a/scripts/build-windows.ps1 b/scripts/build-windows.ps1 new file mode 100644 index 0000000..2bc5705 --- /dev/null +++ b/scripts/build-windows.ps1 @@ -0,0 +1,307 @@ +# SPDX-License-Identifier: Apache-2.0 +[CmdletBinding()] +param( + [Parameter(Mandatory = $true)] + [string]$LLVMDir, + + [string]$BuildDir = "build-win", + [string]$InstallDir = "dist\windows", + [string]$Configuration = "Release", + [string]$Generator = "Ninja Multi-Config", + [string]$Arch = "x64", + [string]$Toolset = "", + [string]$ParserType = "CLI11", + [string]$CompilerSourceDir = "", + [string]$StackAnalyzerSourceDir = "", + [string]$LoggerSourceDir = "", + [string]$VersionOverride = "", + [switch]$PackageZip +) + +$ErrorActionPreference = "Stop" + +function Invoke-NativeCommand { + param( + [Parameter(Mandatory = $true)] + [string]$FilePath, + + [Parameter(ValueFromRemainingArguments = $true)] + [string[]]$Arguments + ) + + & $FilePath @Arguments + if ($LASTEXITCODE -ne 0) + { + throw "Command failed with exit code ${LASTEXITCODE}: $FilePath $($Arguments -join ' ')" + } +} + +function New-PatchedCMakePackageDir { + param( + [Parameter(Mandatory = $true)] + [string]$SourceDir, + + [Parameter(Mandatory = $true)] + [string]$DestinationDir, + + [Parameter(Mandatory = $true)] + [string]$OldValue, + + [Parameter(Mandatory = $true)] + [string]$NewValue, + + [string]$ImportPrefix = "" + ) + + New-Item -ItemType Directory -Force -Path $DestinationDir | Out-Null + Copy-Item -Path (Join-Path $SourceDir "*") -Destination $DestinationDir -Recurse -Force + + Get-ChildItem -Path $DestinationDir -Recurse -Filter *.cmake | ForEach-Object { + $content = Get-Content -Path $_.FullName -Raw + $updated = $content.Replace($OldValue, $NewValue) + if ($ImportPrefix -ne "") + { + $updated = [regex]::Replace( + $updated, + '(?ms)# Compute the installation prefix relative to this file\.\r?\nget_filename_component\(_IMPORT_PREFIX "\$\{CMAKE_CURRENT_LIST_FILE\}" PATH\)\r?\nget_filename_component\(_IMPORT_PREFIX "\$\{_IMPORT_PREFIX\}" PATH\)\r?\nget_filename_component\(_IMPORT_PREFIX "\$\{_IMPORT_PREFIX\}" PATH\)\r?\nget_filename_component\(_IMPORT_PREFIX "\$\{_IMPORT_PREFIX\}" PATH\)\r?\nif\(_IMPORT_PREFIX STREQUAL "/"\)\r?\n\s*set\(_IMPORT_PREFIX ""\)\r?\nendif\(\)', + "# Compute the installation prefix relative to this file.`nset(_IMPORT_PREFIX `"$ImportPrefix`")" + ) + $updated = [regex]::Replace( + $updated, + '(?ms)# Compute the installation prefix from this LLVMConfig\.cmake file location\.\r?\nget_filename_component\(LLVM_INSTALL_PREFIX "\$\{CMAKE_CURRENT_LIST_FILE\}" PATH\)\r?\nget_filename_component\(LLVM_INSTALL_PREFIX "\$\{LLVM_INSTALL_PREFIX\}" PATH\)\r?\nget_filename_component\(LLVM_INSTALL_PREFIX "\$\{LLVM_INSTALL_PREFIX\}" PATH\)\r?\nget_filename_component\(LLVM_INSTALL_PREFIX "\$\{LLVM_INSTALL_PREFIX\}" PATH\)', + "# Compute the installation prefix from this LLVMConfig.cmake file location.`nset(LLVM_INSTALL_PREFIX `"$ImportPrefix`")" + ) + $updated = [regex]::Replace( + $updated, + '(?ms)# Compute the installation prefix from this LLVMConfig\.cmake file location\.\r?\nget_filename_component\(CLANG_INSTALL_PREFIX "\$\{CMAKE_CURRENT_LIST_FILE\}" PATH\)\r?\nget_filename_component\(CLANG_INSTALL_PREFIX "\$\{CLANG_INSTALL_PREFIX\}" PATH\)\r?\nget_filename_component\(CLANG_INSTALL_PREFIX "\$\{CLANG_INSTALL_PREFIX\}" PATH\)\r?\nget_filename_component\(CLANG_INSTALL_PREFIX "\$\{CLANG_INSTALL_PREFIX\}" PATH\)', + "# Compute the installation prefix from this LLVMConfig.cmake file location.`nset(CLANG_INSTALL_PREFIX `"$ImportPrefix`")" + ) + } + if ($updated -ne $content) + { + Set-Content -Path $_.FullName -Value $updated -NoNewline + } + } +} + +function Reset-BuildDirectoryIfGeneratorChanged { + param( + [Parameter(Mandatory = $true)] + [string]$BuildDirectory, + + [Parameter(Mandatory = $true)] + [string]$GeneratorName, + + [string]$PlatformName = "", + + [string]$ToolsetName = "" + ) + + $cachePath = Join-Path $BuildDirectory "CMakeCache.txt" + if (-not (Test-Path $cachePath)) + { + return + } + + $cacheContent = Get-Content $cachePath -ErrorAction Stop + $cachedGeneratorLine = $cacheContent | Where-Object { $_ -like "CMAKE_GENERATOR:INTERNAL=*" } | Select-Object -First 1 + $cachedPlatformLine = $cacheContent | Where-Object { $_ -like "CMAKE_GENERATOR_PLATFORM:INTERNAL=*" } | Select-Object -First 1 + $cachedToolsetLine = $cacheContent | Where-Object { $_ -like "CMAKE_GENERATOR_TOOLSET:INTERNAL=*" } | Select-Object -First 1 + + $cachedGenerator = if ($cachedGeneratorLine) { $cachedGeneratorLine.Split("=", 2)[1] } else { "" } + $cachedPlatform = if ($cachedPlatformLine) { $cachedPlatformLine.Split("=", 2)[1] } else { "" } + $cachedToolset = if ($cachedToolsetLine) { $cachedToolsetLine.Split("=", 2)[1] } else { "" } + + if ($cachedGenerator -eq $GeneratorName -and $cachedPlatform -eq $PlatformName -and $cachedToolset -eq $ToolsetName) + { + return + } + + Write-Host "Resetting build directory '$BuildDirectory' because cached generator/platform/toolset '$cachedGenerator'/'$cachedPlatform'/'$cachedToolset' does not match '$GeneratorName'/'$PlatformName'/'$ToolsetName'." + Remove-Item -LiteralPath $BuildDirectory -Recurse -Force +} + +$repoRoot = Split-Path -Parent $PSScriptRoot +$resolvedBuildDir = Join-Path $repoRoot $BuildDir +$resolvedInstallDir = Join-Path $repoRoot $InstallDir +$installedLLVMDir = (Resolve-Path $LLVMDir).Path +$installedClangDir = Join-Path (Split-Path $installedLLVMDir -Parent) "clang" +$resolvedLLVMDir = $installedLLVMDir +$resolvedClangDir = $installedClangDir +$llvmRoot = Split-Path (Split-Path (Split-Path $resolvedLLVMDir -Parent) -Parent) -Parent +$llvmBinDir = Join-Path $llvmRoot "bin" + +if (Test-Path $llvmBinDir) +{ + $env:PATH = "$llvmBinDir;$env:PATH" +} + +$clangClPath = Join-Path $llvmBinDir "clang-cl.exe" +if (-not (Test-Path $clangClPath)) +{ + throw "clang-cl.exe was not found in '$llvmBinDir'." +} + +$clangExePath = Join-Path $llvmBinDir "clang.exe" + +$vswhere = Join-Path ${env:ProgramFiles(x86)} "Microsoft Visual Studio\Installer\vswhere.exe" +if (-not (Test-Path $vswhere)) +{ + throw "Unable to find vswhere.exe." +} + +$vsPath = & $vswhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath +if (-not $vsPath) +{ + throw "Unable to find a Visual Studio installation with C++ build tools." +} + +$platformForCache = if ($Generator -like "Visual Studio*") { $Arch } else { "" } +$toolsetForCache = if ($Generator -like "Visual Studio*") { $Toolset } else { "" } +Reset-BuildDirectoryIfGeneratorChanged -BuildDirectory $resolvedBuildDir -GeneratorName $Generator -PlatformName $platformForCache -ToolsetName $toolsetForCache + +$diaguidsCandidate = Join-Path $vsPath "DIA SDK\lib\amd64\diaguids.lib" +if (-not (Test-Path $diaguidsCandidate)) +{ + $diaguidsCandidate = Get-ChildItem -Path $vsPath -Recurse -Filter diaguids.lib -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -like "*DIA SDK\\lib\\amd64\\diaguids.lib" } | + Select-Object -First 1 -ExpandProperty FullName +} + +$staleDiaPath = "C:/Program Files (x86)/Microsoft Visual Studio/2019/Professional/DIA SDK/lib/amd64/diaguids.lib" +$llvmExportsPath = Join-Path $installedLLVMDir "LLVMExports.cmake" +if ((Test-Path $llvmExportsPath) -and (Test-Path $diaguidsCandidate)) +{ + $llvmExportsContent = Get-Content -Path $llvmExportsPath -Raw + if ($llvmExportsContent.Contains($staleDiaPath)) + { + $patchedRoot = Join-Path $resolvedBuildDir "__llvm_cmake_patched" + $patchedCMakeRoot = Join-Path $patchedRoot "lib\\cmake" + $patchedLLVMDir = Join-Path $patchedCMakeRoot "llvm" + $patchedClangDir = Join-Path $patchedCMakeRoot "clang" + $replacementDiaPath = $diaguidsCandidate.Replace("\", "/") + $importPrefix = $llvmRoot.Replace("\", "/") + + New-PatchedCMakePackageDir -SourceDir $installedLLVMDir -DestinationDir $patchedLLVMDir -OldValue $staleDiaPath -NewValue $replacementDiaPath -ImportPrefix $importPrefix + New-PatchedCMakePackageDir -SourceDir $installedClangDir -DestinationDir $patchedClangDir -OldValue $staleDiaPath -NewValue $replacementDiaPath -ImportPrefix $importPrefix + + # LLVMConfig.cmake computes LLVM_INSTALL_PREFIX relative to its own location, + # which resolves to __llvm_cmake_patched instead of C:\LLVM. + # Create junctions so paths under the patched root still resolve correctly. + $patchedIncludeDir = Join-Path $patchedRoot "include" + if (-not (Test-Path $patchedIncludeDir)) + { + cmd /c mklink /J "$patchedIncludeDir" "$llvmRoot\include" | Out-Null + } + $patchedBinDir = Join-Path $patchedRoot "bin" + if (-not (Test-Path $patchedBinDir)) + { + cmd /c mklink /J "$patchedBinDir" "$llvmBinDir" | Out-Null + } + + $resolvedLLVMDir = $patchedLLVMDir + $resolvedClangDir = $patchedClangDir + } +} + +$cmakeArgs = @( + "-S", $repoRoot, + "-B", $resolvedBuildDir, + "-G", $Generator, + "-DPARSER_TYPE=$ParserType", + "-DUSE_THREAD_SANITIZER=OFF", + "-DUSE_ADDRESS_SANITIZER=OFF", + "-DLLVM_DIR=$resolvedLLVMDir", + "-DClang_DIR=$resolvedClangDir", + "-DCMAKE_PREFIX_PATH=$llvmRoot", + "-DCLANG_EXECUTABLE=$clangExePath" +) + +if ($CompilerSourceDir -ne "") +{ + $resolvedCompilerSourceDir = (Resolve-Path $CompilerSourceDir).Path + $cmakeArgs += "-DFETCHCONTENT_SOURCE_DIR_CC=$resolvedCompilerSourceDir" +} + +if ($StackAnalyzerSourceDir -ne "") +{ + $resolvedStackAnalyzerSourceDir = (Resolve-Path $StackAnalyzerSourceDir).Path + $cmakeArgs += "-DFETCHCONTENT_SOURCE_DIR_STACK_ANALYZER=$resolvedStackAnalyzerSourceDir" +} + +if ($LoggerSourceDir -ne "") +{ + $resolvedLoggerSourceDir = (Resolve-Path $LoggerSourceDir).Path + $cmakeArgs += "-DFETCHCONTENT_SOURCE_DIR_CORETRACE_LOGGER=$resolvedLoggerSourceDir" +} + +if ($Generator -like "Visual Studio*") +{ + $cmakeArgs += @("-A", $Arch) + if ($Toolset -ne "") + { + $cmakeArgs += @("-T", $Toolset) + } +} +else +{ + $cmakeArgs += @( + "-DCMAKE_C_COMPILER=$clangClPath", + "-DCMAKE_CXX_COMPILER=$clangClPath" + ) +} + +if ($VersionOverride -ne "") +{ + $cmakeArgs += "-DCORETRACE_VERSION_OVERRIDE=$VersionOverride" +} + +if ($Generator -like "Visual Studio*") +{ + Invoke-NativeCommand cmake @cmakeArgs +} +else +{ + $devShell = Join-Path $vsPath "Common7\Tools\Launch-VsDevShell.ps1" + + if (Test-Path $devShell) + { + try + { + . $devShell -Arch amd64 -HostArch amd64 | Out-Null + } + catch + { + Write-Warning "Could not start Developer PowerShell using script path '$devShell'. Attempting to continue." + } + } + else + { + Write-Warning "Launch-VsDevShell.ps1 not found at '$devShell'. Attempting to continue." + } + + Invoke-NativeCommand cmake @cmakeArgs +} + +Invoke-NativeCommand cmake --build $resolvedBuildDir --config $Configuration +Invoke-NativeCommand cmake --install $resolvedBuildDir --config $Configuration --prefix $resolvedInstallDir + +$binaryPath = Join-Path $resolvedInstallDir "bin\ctrace.exe" +if (-not (Test-Path $binaryPath)) +{ + throw "Expected output binary was not produced: $binaryPath" +} + +if ($PackageZip) +{ + $zipPath = Join-Path $repoRoot "coretrace-windows-$Configuration.zip" + if (Test-Path $zipPath) + { + Remove-Item -LiteralPath $zipPath -Force + } + Compress-Archive -Path (Join-Path $resolvedInstallDir "*") -DestinationPath $zipPath + Write-Host "Created package: $zipPath" +} + +Write-Host "Build completed successfully." +Write-Host "Executable: $binaryPath" diff --git a/src/App/ToolConfig.cpp b/src/App/ToolConfig.cpp index 51346a2..5140461 100644 --- a/src/App/ToolConfig.cpp +++ b/src/App/ToolConfig.cpp @@ -865,7 +865,10 @@ namespace ctrace } if (hasValue) { - config.global.ipcPath = stringValue; + if (!stringValue.empty()) + { + config.global.ipcPath = stringValue; + } } return true; diff --git a/src/ArgumentParser/ArgumentParserFactory.cpp b/src/ArgumentParser/ArgumentParserFactory.cpp index c5563ec..62fb223 100644 --- a/src/ArgumentParser/ArgumentParserFactory.cpp +++ b/src/ArgumentParser/ArgumentParserFactory.cpp @@ -1,7 +1,13 @@ // SPDX-License-Identifier: Apache-2.0 #include "ArgumentParser/ArgumentParserFactory.hpp" + +#if !defined(USE_GETOPT) #include "ArgumentParser/CLI11/CLI11ArgumentParser.hpp" +#endif + +#if defined(USE_GETOPT) #include "ArgumentParser/GetOpt/GetOptArgumentParser.hpp" +#endif std::unique_ptr createArgumentParser() { diff --git a/src/Process/Tools/TscancodeToolImplementation.cpp b/src/Process/Tools/TscancodeToolImplementation.cpp index 7780f00..bc57ce9 100644 --- a/src/Process/Tools/TscancodeToolImplementation.cpp +++ b/src/Process/Tools/TscancodeToolImplementation.cpp @@ -20,8 +20,10 @@ namespace ctrace argsProcess.push_back("--enable=all"); argsProcess.push_back(src_file); - auto process = ProcessFactory::createProcess( - "./tscancode/src/tscancode/trunk/tscancode", argsProcess); + const auto command = ctrace::resolveTscancodeCommand(); + argsProcess.insert(argsProcess.begin(), command.prefixArguments.begin(), + command.prefixArguments.end()); + auto process = ProcessFactory::createProcess(command.executable, argsProcess); process->execute(); ctrace::Thread::Output::tool_out(process->logOutput); ctrace::Thread::Output::cout("Finished tscancode on " + file);