diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cd29c13..8fd4a37 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -13,14 +13,14 @@ repos: # This brings in a portable version of clang-format. # See also: https://github.com/ssciwr/clang-format-wheel - repo: https://github.com/pre-commit/mirrors-clang-format - rev: v18.1.8 + rev: v21.1.2 hooks: - id: clang-format types_or: [c++, c] # CMake linting and formatting - repo: https://github.com/BlankSpruce/gersemi - rev: 0.15.1 + rev: 0.22.3 hooks: - id: gersemi name: CMake linting @@ -28,13 +28,13 @@ repos: # Markdown linting # Config file: .markdownlint.yaml - repo: https://github.com/igorshubovych/markdownlint-cli - rev: v0.42.0 + rev: v0.45.0 hooks: - id: markdownlint - repo: https://github.com/codespell-project/codespell - rev: v2.3.0 + rev: v2.4.1 hooks: - id: codespell -exclude: 'cookiecutter/' +exclude: 'cookiecutter/|infra/' diff --git a/infra/.beman_submodule b/infra/.beman_submodule index 7d06f0a..bfed167 100644 --- a/infra/.beman_submodule +++ b/infra/.beman_submodule @@ -1,3 +1,3 @@ [beman_submodule] remote=https://github.com/bemanproject/infra.git -commit_hash=3dc3acd501eccaf3c9bd8d93edb1ebf1fa58d8c6 +commit_hash=bb58b2a1cc894d58a55bf745be78f5d27029e245 diff --git a/infra/.devcontainer/docker_dev_container/devcontainer.json b/infra/.devcontainer/docker_dev_container/devcontainer.json deleted file mode 100644 index 6707a25..0000000 --- a/infra/.devcontainer/docker_dev_container/devcontainer.json +++ /dev/null @@ -1,13 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{ - "name": "beman.infra Docker Devcontainer", - "image": "mcr.microsoft.com/devcontainers/base:ubuntu", - "features": { - "ghcr.io/devcontainers/features/docker-in-docker:2": { - "version": "latest", - "enableNonRootDocker": true, - "moby": false - }, - "ghcr.io/devcontainers-extra/features/pre-commit:2": {} - } -} diff --git a/infra/.github/CODEOWNERS b/infra/.github/CODEOWNERS index d3be6f1..4ff90a4 100644 --- a/infra/.github/CODEOWNERS +++ b/infra/.github/CODEOWNERS @@ -1 +1 @@ -* @bemanproject/core-reviewers +* @ednolan @neatudarius @rishyak @wusatosi @JeffGarland diff --git a/infra/.github/workflows/beman-submodule.yml b/infra/.github/workflows/beman-submodule.yml index d18dd7f..8435086 100644 --- a/infra/.github/workflows/beman-submodule.yml +++ b/infra/.github/workflows/beman-submodule.yml @@ -4,6 +4,8 @@ name: beman-submodule tests on: push: + branches: + - main pull_request: workflow_dispatch: diff --git a/infra/.github/workflows/build_devcontainer.yml b/infra/.github/workflows/build_devcontainer.yml deleted file mode 100644 index 1f26fc7..0000000 --- a/infra/.github/workflows/build_devcontainer.yml +++ /dev/null @@ -1,97 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -name: Publish Beman Containers - -on: - push: - paths: - - ".github/workflows/build_devcontainer.yml" - - "containers/**" - workflow_dispatch: - -env: - REGISTRY: ghcr.io - DEBUG_NAME: ${{ github.repository }} - DEPLOY_DEV_NAME_PREFIX: bemanproject/devcontainers - DEPLOY_TESTING_NAME_PREFIX: bemanproject/testingcontainers - BASE_IMAGE_DEV: mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04 - BASE_IMAGE_TEST: ubuntu:24.04 - -permissions: - packages: write - -jobs: - containers: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - compilers: - - kind: gcc - version: 14 - - kind: gcc - version: 13 - - kind: gcc - version: 12 - - kind: gcc - version: 11 - - kind: clang - version: 21 - - kind: clang - version: 20 - - kind: clang - version: 19 - - kind: clang - version: 18 - - kind: clang - version: 17 - usage: [dev, test] - name: "${{ matrix.usage }}: ${{ matrix.compilers.kind }}-${{ matrix.compilers.version }}" - steps: - - name: Compute Image Name - id: image_name - run: | - if [ "${{ github.repository }}/${{ github.ref }}" != "bemanproject/infra/refs/heads/main" ]; then - image_name="${{ env.DEBUG_NAME }}" - tag="${{ matrix.usage }}-${{ matrix.compilers.kind }}-${{ matrix.compilers.version }}" - else - if [ "${{ matrix.usage }}" = "dev" ]; then - image_name="${{ env.DEPLOY_DEV_NAME_PREFIX }}-${{ matrix.compilers.kind }}" - else - image_name="${{ env.DEPLOY_TESTING_NAME_PREFIX }}-${{ matrix.compilers.kind }}" - fi - tag="${{ matrix.compilers.version }}" - fi - - echo "Image Name: $image_name, Tag: $tag" - - echo "image_name=$image_name" >> "$GITHUB_OUTPUT" - echo "tag=$tag" >> "$GITHUB_OUTPUT" - - name: Compute Image base - id: image_base - run: | - if [ "${{ matrix.usage }}" == "dev" ]; then - echo "image=${{ env.BASE_IMAGE_DEV }}" >> "$GITHUB_OUTPUT" - else - echo "image=${{ env.BASE_IMAGE_TEST }}" >> "$GITHUB_OUTPUT" - fi - - name: Checkout repository - uses: actions/checkout@v4 - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build and push Docker image - uses: docker/build-push-action@v6 - with: - context: containers - build-args: | - base_image=${{ steps.image_base.outputs.image }} - compiler_kind=${{ matrix.compilers.kind }} - compiler_version=${{ matrix.compilers.version }} - push: true - tags: ${{ env.REGISTRY }}/${{ steps.image_name.outputs.image_name }}:${{ steps.image_name.outputs.tag }} - # https://github.com/docker/build-push-action/issues/894 - provenance: false diff --git a/infra/.github/workflows/pre-commit.yml b/infra/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..9646831 --- /dev/null +++ b/infra/.github/workflows/pre-commit.yml @@ -0,0 +1,78 @@ +name: Lint Check (pre-commit) + +on: + # We have to use pull_request_target here as pull_request does not grant + # enough permission for reviewdog + pull_request_target: + push: + branches: + - main + +jobs: + pre-commit-push: + name: Pre-Commit check on Push + runs-on: ubuntu-latest + if: ${{ github.event_name == 'push' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + # We wish to run pre-commit on all files instead of the changes + # only made in the push commit. + # + # So linting error persists when there's formatting problem. + - uses: pre-commit/action@v3.0.1 + + pre-commit-pr: + name: Pre-Commit check on PR + runs-on: ubuntu-latest + if: ${{ github.event_name == 'pull_request_target' }} + + permissions: + contents: read + checks: write + issues: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + # pull_request_target checkout the base of the repo + # We need to checkout the actual pr to lint the changes. + - name: Checkout pr + run: gh pr checkout ${{ github.event.number }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: 3.13 + + # we only lint on the changed file in PR. + - name: Get Changed Files + id: changed-files + uses: tj-actions/changed-files@v45 + + # See: + # https://github.com/tj-actions/changed-files?tab=readme-ov-file#using-local-git-directory- + - uses: pre-commit/action@v3.0.1 + id: run-pre-commit + with: + extra_args: --files ${{ steps.changed-files.outputs.all_changed_files }} + + # Review dog posts the suggested change from pre-commit to the pr. + - name: suggester / pre-commit + uses: reviewdog/action-suggester@v1 + if: ${{ failure() && steps.run-pre-commit.conclusion == 'failure' }} + with: + tool_name: pre-commit + level: warning + reviewdog_flags: "-fail-level=error" diff --git a/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml b/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml new file mode 100644 index 0000000..024a51f --- /dev/null +++ b/infra/.github/workflows/reusable-beman-create-issue-when-fault.yml @@ -0,0 +1,28 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception + +name: 'Beman issue creation workflow' +on: + workflow_call: + workflow_dispatch: +jobs: + create-issue: + runs-on: ubuntu-latest + steps: + # See https://github.com/cli/cli/issues/5075 + - uses: actions/checkout@v4 + - name: Create issue + run: | + issue_num=$(gh issue list -s open -S "[SCHEDULED-BUILD] infra repo CI job failure" -L 1 --json number | jq 'if length == 0 then -1 else .[0].number end') + body="**CI job failure Report** + - **Time of Failure**: $(date -u '+%B %d, %Y, %H:%M %Z') + - **Commit**: [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) + - **Action Run**: [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) + The scheduled job triggered by cron has failed. + Please investigate the logs and recent changes associated with this commit or rerun the workflow if you believe this is an error." + if [[ $issue_num -eq -1 ]]; then + gh issue create --repo ${{ github.repository }} --title "[SCHEDULED-BUILD] infra repo CI job failure" --body "$body" --assignee ${{ github.actor }} + else + gh issue comment --repo ${{ github.repository }} $issue_num --body "$body" + fi + env: + GH_TOKEN: ${{ github.token }} diff --git a/infra/.gitignore b/infra/.gitignore index 259148f..b7cdbb5 100644 --- a/infra/.gitignore +++ b/infra/.gitignore @@ -30,3 +30,30 @@ *.exe *.out *.app + +# Python +__pycache__/ +.pytest_cache/ +*.pyc +*.pyo +*.pyd +*.pyw +*.pyz +*.pywz +*.pyzw +*.pyzwz +*.delete_me + +# MAC OS +*.DS_Store + +# Editor files +.vscode/ +.idea/ + +# Build directories +infra.egg-info/ +beman_tidy.egg-info/ +*.egg-info/ +build/ +dist/ diff --git a/infra/.markdownlint.yaml b/infra/.markdownlint.yaml deleted file mode 100644 index 81f5fcd..0000000 --- a/infra/.markdownlint.yaml +++ /dev/null @@ -1,9 +0,0 @@ -# MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.35.0/doc/md033.md -# Disable inline html linter is needed for
-MD033: false - -# MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.35.0/doc/md013.md -# Conforms to .clang-format ColumnLimit -# Update the comment in .clang-format if we no-longer tie these two column limits. -MD013: - line_length: 119 diff --git a/infra/.pre-commit-config.yaml b/infra/.pre-commit-config.yaml new file mode 100644 index 0000000..e806e59 --- /dev/null +++ b/infra/.pre-commit-config.yaml @@ -0,0 +1,32 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + + # CMake linting and formatting + - repo: https://github.com/BlankSpruce/gersemi + rev: 0.22.3 + hooks: + - id: gersemi + name: CMake linting + exclude: ^.*/tests/.*/data/ # Exclude test data directories + + # Python linting and formatting + # config file: ruff.toml (not currently present but add if needed) + # https://docs.astral.sh/ruff/configuration/ + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.13.2 + hooks: + - id: ruff-check + files: ^tools/beman-tidy/ + - id: ruff-format + files: ^tools/beman-tidy/ diff --git a/infra/.pre-commit-hooks.yaml b/infra/.pre-commit-hooks.yaml new file mode 100644 index 0000000..d327587 --- /dev/null +++ b/infra/.pre-commit-hooks.yaml @@ -0,0 +1,7 @@ +- id: beman-tidy + name: "beman-tidy: bemanification your repo" + entry: ./tools/beman-tidy/beman-tidy + language: script + pass_filenames: false + always_run: true + args: [".", "--verbose"] diff --git a/infra/LICENSE b/infra/LICENSE index 111a208..f6db814 100644 --- a/infra/LICENSE +++ b/infra/LICENSE @@ -1,3 +1,4 @@ + Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -216,15 +217,3 @@ conflicts with the conditions of the GPLv2, you may retroactively and prospectively choose to deem waived or otherwise exclude such Section(s) of the License, but only in their entirety and only with respect to the Combined Software. - -============================================================================== -Software from third parties included in the Beman Project: -============================================================================== -The Beman Project contains third party software which is under different license -terms. All such code will be identified clearly using at least one of two -mechanisms: -1) It will be in a separate directory tree with its own `LICENSE.txt` or - `LICENSE` file at the top containing the specific license and restrictions - which apply to that software, or -2) It will contain specific license and restriction terms at the top of every - file. diff --git a/infra/README.md b/infra/README.md index 4d31a49..16b2672 100644 --- a/infra/README.md +++ b/infra/README.md @@ -2,10 +2,54 @@ -This repository contains the infrastructure for The Beman Project. This is NOT a library repository, so it does not -respect the usual structure of a Beman library repository nor The Beman Standard. +This repository contains the infrastructure for The Beman Project. This is NOT a library repository, +so it does not respect the usual structure of a Beman library repository nor The Beman Standard! ## Description +* `cmake/`: CMake modules and toolchain files used by Beman libraries. * `containers/`: Containers used for CI builds and tests in the Beman org. * `tools/`: Tools used to manage the infrastructure and the codebase (e.g., linting, formatting, etc.). + +## Usage + +This repository is intended to be used as a beman-submodule in other Beman repositories. See +[the Beman Submodule documentation](./tools/beman-submodule/README.md) for details. + + +### CMake Modules + + +#### `beman_install_library` + +The CMake modules in this repository are intended to be used by Beman libraries. Use the +`beman_add_install_library_config()` function to install your library, along with header +files, any metadata files, and a CMake config file for `find_package()` support. + +```cmake +add_library(beman.something) +add_library(beman::something ALIAS beman.something) + +# ... configure your target as needed ... + +find_package(beman-install-library REQUIRED) +beman_install_library(beman.something) +``` + +Note that the target must be created before calling `beman_install_library()`. The module +also assumes that the target is named using the `beman.something` convention, and it +uses that assumption to derive the names to match other Beman standards and conventions. +If your target does not follow that convention, raise an issue or pull request to add +more configurability to the module. + +The module will configure the target to install: + +* The library target itself +* Any public headers associated with the target +* CMake files for `find_package(beman.something)` support + +Some options for the project and target will also be supported: + +* `BEMAN_INSTALL_CONFIG_FILE_PACKAGES` - a list of package names (e.g., `beman.something`) for which to install the config file + (default: all packages) +* `_INSTALL_CONFIG_FILE_PACKAGE` - a per-project option to enable/disable config file installation (default: `ON` if the project is top-level, `OFF` otherwise). For instance for `beman.something`, the option would be `BEMAN_SOMETHING_INSTALL_CONFIG_FILE_PACKAGE`. diff --git a/infra/cmake/appleclang-toolchain.cmake b/infra/cmake/appleclang-toolchain.cmake index 5f44e80..70ef548 100644 --- a/infra/cmake/appleclang-toolchain.cmake +++ b/infra/cmake/appleclang-toolchain.cmake @@ -39,3 +39,6 @@ set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/beman-install-library-config.cmake b/infra/cmake/beman-install-library-config.cmake new file mode 100644 index 0000000..e7fd0ad --- /dev/null +++ b/infra/cmake/beman-install-library-config.cmake @@ -0,0 +1,169 @@ +# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +include_guard(GLOBAL) + +# This file defines the function `beman_install_library` which is used to +# install a library target and its headers, along with optional CMake +# configuration files. +# +# The function is designed to be reusable across different Beman libraries. + +function(beman_install_library name) + # Usage + # ----- + # + # beman_install_library(NAME) + # + # Brief + # ----- + # + # This function installs the specified library target and its headers. + # It also handles the installation of the CMake configuration files if needed. + # + # CMake variables + # --------------- + # + # Note that configuration of the installation is generally controlled by CMake + # cache variables so that they can be controlled by the user or tool running the + # `cmake` command. Neither `CMakeLists.txt` nor `*.cmake` files should set these + # variables directly. + # + # - BEMAN_INSTALL_CONFIG_FILE_PACKAGES: + # List of packages that require config file installation. + # If the package name is in this list, it will install the config file. + # + # - _INSTALL_CONFIG_FILE_PACKAGE: + # Boolean to control config file installation for the specific library. + # The prefix `` is the uppercased name of the library with dots + # replaced by underscores. + # + if(NOT TARGET "${name}") + message(FATAL_ERROR "Target '${name}' does not exist.") + endif() + + if(NOT ARGN STREQUAL "") + message( + FATAL_ERROR + "beman_install_library does not accept extra arguments: ${ARGN}" + ) + endif() + + # Given foo.bar, the component name is bar + string(REPLACE "." ";" name_parts "${name}") + # fail if the name doesn't look like foo.bar + list(LENGTH name_parts name_parts_length) + if(NOT name_parts_length EQUAL 2) + message( + FATAL_ERROR + "beman_install_library expects a name of the form 'beman.', got '${name}'" + ) + endif() + + set(target_name "${name}") + set(install_component_name "${name}") + set(export_name "${name}") + set(package_name "${name}") + list(GET name_parts -1 component_name) + + install( + TARGETS "${target_name}" + COMPONENT "${install_component_name}" + EXPORT "${export_name}" + FILE_SET HEADERS + ) + + set_target_properties( + "${target_name}" + PROPERTIES EXPORT_NAME "${component_name}" + ) + + include(GNUInstallDirs) + + # Determine the prefix for project-specific variables + string(TOUPPER "${name}" project_prefix) + string(REPLACE "." "_" project_prefix "${project_prefix}") + + option( + ${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE + "Enable building examples. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." + ${PROJECT_IS_TOP_LEVEL} + ) + + # By default, install the config package + set(install_config_package ON) + + # Turn OFF installation of config package by default if, + # in order of precedence: + # 1. The specific package variable is set to OFF + # 2. The package name is not in the list of packages to install config files + if(DEFINED BEMAN_INSTALL_CONFIG_FILE_PACKAGES) + if( + NOT "${install_component_name}" + IN_LIST + BEMAN_INSTALL_CONFIG_FILE_PACKAGES + ) + set(install_config_package OFF) + endif() + endif() + if(DEFINED ${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE) + set(install_config_package + ${${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE} + ) + endif() + + if(install_config_package) + message( + DEBUG + "beman-install-library: Installing a config package for '${name}'" + ) + + include(CMakePackageConfigHelpers) + + find_file( + config_file_template + NAMES "${package_name}-config.cmake.in" + PATHS "${CMAKE_CURRENT_SOURCE_DIR}" + NO_DEFAULT_PATH + NO_CACHE + REQUIRED + ) + set(config_package_file + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}-config.cmake" + ) + set(package_install_dir "${CMAKE_INSTALL_LIBDIR}/cmake/${package_name}") + configure_package_config_file( + "${config_file_template}" + "${config_package_file}" + INSTALL_DESTINATION "${package_install_dir}" + PATH_VARS PROJECT_NAME PROJECT_VERSION + ) + + set(config_version_file + "${CMAKE_CURRENT_BINARY_DIR}/${package_name}-config-version.cmake" + ) + write_basic_package_version_file( + "${config_version_file}" + VERSION "${PROJECT_VERSION}" + COMPATIBILITY ExactVersion + ) + + install( + FILES "${config_package_file}" "${config_version_file}" + DESTINATION "${package_install_dir}" + COMPONENT "${install_component_name}" + ) + + set(config_targets_file "${package_name}-targets.cmake") + install( + EXPORT "${export_name}" + DESTINATION "${package_install_dir}" + NAMESPACE beman:: + FILE "${config_targets_file}" + COMPONENT "${install_component_name}" + ) + else() + message( + DEBUG + "beman-install-library: Not installing a config package for '${name}'" + ) + endif() +endfunction() diff --git a/infra/cmake/gnu-toolchain.cmake b/infra/cmake/gnu-toolchain.cmake index b6dddf6..d3b9f92 100644 --- a/infra/cmake/gnu-toolchain.cmake +++ b/infra/cmake/gnu-toolchain.cmake @@ -36,3 +36,6 @@ set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/llvm-libc++-toolchain.cmake b/infra/cmake/llvm-libc++-toolchain.cmake new file mode 100644 index 0000000..76264c6 --- /dev/null +++ b/infra/cmake/llvm-libc++-toolchain.cmake @@ -0,0 +1,20 @@ +# SPDX-License-Identifier: BSL-1.0 + +# This toolchain file is not meant to be used directly, +# but to be invoked by CMake preset and GitHub CI. +# +# This toolchain file configures for LLVM family of compiler. +# +# BEMAN_BUILDSYS_SANITIZER: +# This optional CMake parameter is not meant for public use and is subject to +# change. +# Possible values: +# - MaxSan: configures clang and clang++ to use all available non-conflicting +# sanitizers. +# - TSan: configures clang and clang++ to enable the use of thread sanitizer. + +include(${CMAKE_CURRENT_LIST_DIR}/llvm-toolchain.cmake) + +if(NOT CMAKE_CXX_FLAGS MATCHES "-stdlib=libc\\+\\+") + string(APPEND CMAKE_CXX_FLAGS " -stdlib=libc++") +endif() diff --git a/infra/cmake/llvm-toolchain.cmake b/infra/cmake/llvm-toolchain.cmake index 5f5ee4b..f1623b7 100644 --- a/infra/cmake/llvm-toolchain.cmake +++ b/infra/cmake/llvm-toolchain.cmake @@ -36,3 +36,6 @@ set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/msvc-toolchain.cmake b/infra/cmake/msvc-toolchain.cmake index c2fffa7..bdc24de 100644 --- a/infra/cmake/msvc-toolchain.cmake +++ b/infra/cmake/msvc-toolchain.cmake @@ -36,3 +36,6 @@ set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/cmake/use-fetch-content.cmake b/infra/cmake/use-fetch-content.cmake index 82c5db2..4ed4839 100644 --- a/infra/cmake/use-fetch-content.cmake +++ b/infra/cmake/use-fetch-content.cmake @@ -59,6 +59,10 @@ function(BemanExemplar_provideDependency method package_name) message(FATAL_ERROR "${BemanExemplar_lockfile}: ${BemanExemplar_error}") endif() + if(BemanExemplar_numDependencies EQUAL 0) + return() + endif() + # Loop over each dependency object math(EXPR BemanExemplar_maxIndex "${BemanExemplar_numDependencies} - 1") foreach(BemanExemplar_index RANGE "${BemanExemplar_maxIndex}") @@ -148,7 +152,12 @@ function(BemanExemplar_provideDependency method package_name) APPEND BemanExemplar_debug "Redirecting find_package calls for ${BemanExemplar_pkgName} " - "to FetchContent logic fetching ${BemanExemplar_repo} at " + "to FetchContent logic.\n" + ) + string( + APPEND + BemanExemplar_debug + "Fetching ${BemanExemplar_repo} at " "${BemanExemplar_tag} according to ${BemanExemplar_lockfile}." ) message(DEBUG "${BemanExemplar_debug}") @@ -173,3 +182,6 @@ cmake_language( SET_DEPENDENCY_PROVIDER BemanExemplar_provideDependency SUPPORTED_METHODS FIND_PACKAGE ) + +# Add this dir to the module path so that `find_package(beman-install-library)` works +list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") diff --git a/infra/containers/Dockerfile b/infra/containers/Dockerfile deleted file mode 100644 index f5f5f9d..0000000 --- a/infra/containers/Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -ARG base_image=mcr.microsoft.com/devcontainers/cpp:1-ubuntu-24.04 -FROM ${base_image} - -# Create the vscode user -RUN bash </dev/null; then - apt-get update && apt-get install -y sudo adduser - useradd -ms /bin/bash -p "" vscode && usermod -aG sudo vscode - fi -EOF - -USER vscode -WORKDIR /tmp - -COPY install_sys.sh . -RUN bash install_sys.sh - -# Newer gcc/ clang is needed to avoid ASAN Stalling, which is turned on by default across beman projects. -# See: https://github.com/google/sanitizers/issues/1614 -# Minimal version: clang-18.1.3, gcc-13.2 -ARG compiler_kind=gcc -ARG compiler_version=14 - -COPY install_compiler.sh . -RUN bash install_compiler.sh ${compiler_kind} ${compiler_version} - -# Common dependency: google-test -RUN sudo apt-get install -y libgtest-dev - -# Pre-commit is beman library's standard linting tool -RUN sudo apt-get install -y pipx -RUN pipx install pre-commit -ENV PATH="/home/vscode/.local/bin:${PATH}" - -ENTRYPOINT ["/usr/bin/bash"] diff --git a/infra/containers/README.md b/infra/containers/README.md deleted file mode 100644 index 83b357d..0000000 --- a/infra/containers/README.md +++ /dev/null @@ -1,46 +0,0 @@ -# Containers - - - -This folder contains the infrastructure for Beman project's -generic container images. You can checkout available images in beman's -[GitHub Packages page](https://github.com/orgs/bemanproject/packages). - -These images includes: - -- The latest CMake from kitware's apt repository -- Latest compiler based on build args (gcc or clang) installed from the universe repository -- [pre-commit](https://pre-commit.com/), the standard linter manager across Beman - -## Devcontainer - -The image is build on top of GitHub's -[C++ devcontainer image](https://github.com/devcontainers/images/tree/main/src/cpp) -for Ubuntu 24.04. - -### Example devcontainer setup - -```json -// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception -{ - "name": "Beman Generic Devcontainer", - "image": "ghcr.io/bemanproject/devcontainers-gcc:14", - "postCreateCommand": "pre-commit", - "customizations": { - "vscode": { - "extensions": [ - "ms-vscode.cpptools", - "ms-vscode.cmake-tools" - ] - } - } -} -``` - -### Building your own image - -You can build your own Beman devcontainer image with: - -```bash -docker build devcontainer/ -``` diff --git a/infra/containers/install_compiler.sh b/infra/containers/install_compiler.sh deleted file mode 100644 index bef0e1c..0000000 --- a/infra/containers/install_compiler.sh +++ /dev/null @@ -1,39 +0,0 @@ -#!/bin/bash -# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception - -set -e -set +x -TOOL=$1 -VERSION=$2 - -echo "Install ${TOOL} at: ${VERSION}" - -shopt -s nocasematch -if [ "$TOOL" = "gcc" ]; then - sudo apt-get remove -y gcc-"$VERSION" g++-"$VERSION" gcc g++ - sudo apt-get install -y gcc-"$VERSION" g++-"$VERSION" lcov - - sudo rm -f /usr/bin/gcc - sudo rm -f /usr/bin/g++ - sudo rm -f /usr/bin/gcov - - sudo ln -s "$(which gcc-"$VERSION")" /usr/bin/gcc - sudo ln -s "$(which g++-"$VERSION")" /usr/bin/g++ - sudo ln -s "$(which gcov-"$VERSION")" /usr/bin/gcov - - gcc --version -else - sudo apt-get install -y lsb-release wget software-properties-common gnupg - wget https://apt.llvm.org/llvm.sh - - sudo bash llvm.sh "${VERSION}" - sudo apt-get install -y libc++-"$VERSION"-dev clang-tools-"$VERSION" lcov - - sudo rm -f /usr/bin/clang - sudo rm -f /usr/bin/clang++ - - sudo ln -s "$(which clang-"$VERSION")" /usr/bin/clang - sudo ln -s "$(which clang++-"$VERSION")" /usr/bin/clang++ - - clang --version -fi diff --git a/infra/containers/install_sys.sh b/infra/containers/install_sys.sh deleted file mode 100644 index 0e31aca..0000000 --- a/infra/containers/install_sys.sh +++ /dev/null @@ -1,10 +0,0 @@ -# Install Basic utilities -sudo apt-get install -y ca-certificates gpg wget git curl - -# Install Latest CMake -wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | gpg --dearmor - | sudo tee /usr/share/keyrings/kitware-archive-keyring.gpg >/dev/null -echo 'deb [signed-by=/usr/share/keyrings/kitware-archive-keyring.gpg] https://apt.kitware.com/ubuntu/ noble main' | sudo tee /etc/apt/sources.list.d/kitware.list >/dev/null -sudo apt-get update && sudo apt-get install -y cmake - -# Install Ninja -sudo apt-get install -y ninja-build diff --git a/infra/tools/beman-submodule/beman-submodule b/infra/tools/beman-submodule/beman-submodule index 2007fc4..66cb96e 100755 --- a/infra/tools/beman-submodule/beman-submodule +++ b/infra/tools/beman-submodule/beman-submodule @@ -5,29 +5,39 @@ import argparse import configparser import filecmp +import glob import os -import pathlib import shutil import subprocess import sys import tempfile +from pathlib import Path -def directory_compare(dir1, dir2, ignore): - compared = filecmp.dircmp(dir1, dir2, ignore=ignore) - if compared.left_only or compared.right_only or compared.diff_files: + +def directory_compare( + reference: str | Path, actual: str | Path, ignore, allow_untracked_files: bool): + reference, actual = Path(reference), Path(actual) + + compared = filecmp.dircmp(reference, actual, ignore=ignore) + if (compared.left_only + or (compared.right_only and not allow_untracked_files) + or compared.diff_files): return False for common_dir in compared.common_dirs: - path1 = os.path.join(dir1, common_dir) - path2 = os.path.join(dir2, common_dir) - if not directory_compare(path1, path2, ignore): + path1 = reference / common_dir + path2 = actual / common_dir + if not directory_compare(path1, path2, ignore, allow_untracked_files): return False return True class BemanSubmodule: - def __init__(self, dirpath, remote, commit_hash): - self.dirpath = dirpath + def __init__( + self, dirpath: str | Path, remote: str, commit_hash: str, + allow_untracked_files: bool): + self.dirpath = Path(dirpath) self.remote = remote self.commit_hash = commit_hash + self.allow_untracked_files = allow_untracked_files def parse_beman_submodule_file(path): config = configparser.ConfigParser() @@ -42,23 +52,30 @@ def parse_beman_submodule_file(path): fail() if not 'commit_hash' in config['beman_submodule']: fail() + allow_untracked_files = config.getboolean( + 'beman_submodule', 'allow_untracked_files', fallback=False) return BemanSubmodule( - str(pathlib.Path(path).resolve().parent), - config['beman_submodule']['remote'], config['beman_submodule']['commit_hash']) + Path(path).resolve().parent, + config['beman_submodule']['remote'], + config['beman_submodule']['commit_hash'], + allow_untracked_files) -def get_beman_submodule(dir): - beman_submodule_filepath = os.path.join(dir, '.beman_submodule') - if os.path.isfile(beman_submodule_filepath): +def get_beman_submodule(path: str | Path): + beman_submodule_filepath = Path(path) / '.beman_submodule' + + if beman_submodule_filepath.is_file(): return parse_beman_submodule_file(beman_submodule_filepath) else: return None -def find_beman_submodules_in(dir): - assert os.path.isdir(dir) +def find_beman_submodules_in(path): + path = Path(path) + assert path.is_dir() + result = [] - for dirpath, _, filenames in os.walk(dir): + for dirpath, _, filenames in path.walk(): if '.beman_submodule' in filenames: - result.append(parse_beman_submodule_file(os.path.join(dirpath, '.beman_submodule'))) + result.append(parse_beman_submodule_file(dirpath / '.beman_submodule')) return sorted(result, key=lambda module: module.dirpath) def cwd_git_repository_path(): @@ -83,28 +100,52 @@ def clone_beman_submodule_into_tmpdir(beman_submodule, remote): capture_output=True, check=True) return tmpdir +def get_paths(beman_submodule): + tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) + paths = set(glob.glob('*', root_dir=Path(tmpdir.name), include_hidden=True)) + paths.remove('.git') + return paths + def beman_submodule_status(beman_submodule): tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) - if directory_compare(tmpdir.name, beman_submodule.dirpath, ['.beman_submodule', '.git']): + if directory_compare( + tmpdir.name, beman_submodule.dirpath, ['.beman_submodule', '.git'], + beman_submodule.allow_untracked_files): status_character=' ' else: status_character='+' parent_repo_path = cwd_git_repository_path() if not parent_repo_path: raise Exception('this is not a git repository') - relpath = pathlib.Path( - beman_submodule.dirpath).relative_to(pathlib.Path(parent_repo_path)) + relpath = Path(beman_submodule.dirpath).relative_to(Path(parent_repo_path)) return status_character + ' ' + beman_submodule.commit_hash + ' ' + str(relpath) def beman_submodule_update(beman_submodule, remote): tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, remote) - shutil.rmtree(beman_submodule.dirpath) - with open(os.path.join(tmpdir.name, '.beman_submodule'), 'w') as f: + tmp_path = Path(tmpdir.name) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmp_path) + + if beman_submodule.allow_untracked_files: + for path in get_paths(beman_submodule): + path2 = Path(beman_submodule.dirpath) / path + if Path(path2).is_dir(): + shutil.rmtree(path2) + elif Path(path2).is_file(): + os.remove(path2) + else: + shutil.rmtree(beman_submodule.dirpath) + + submodule_path = tmp_path / '.beman_submodule' + with open(submodule_path, 'w') as f: f.write('[beman_submodule]\n') f.write(f'remote={beman_submodule.remote}\n') - f.write(f'commit_hash={beman_submodule.commit_hash}\n') - shutil.rmtree(os.path.join(tmpdir.name, '.git')) - shutil.copytree(tmpdir.name, beman_submodule.dirpath) + f.write(f'commit_hash={sha_process.stdout.strip()}\n') + if beman_submodule.allow_untracked_files: + f.write(f'allow_untracked_files=True\n') + shutil.rmtree(tmp_path / '.git') + shutil.copytree(tmp_path, beman_submodule.dirpath, dirs_exist_ok=True) def update_command(remote, path): if not path: @@ -120,25 +161,29 @@ def update_command(remote, path): for beman_submodule in beman_submodules: beman_submodule_update(beman_submodule, remote) -def add_command(repository, path): +def add_command(repository, path, allow_untracked_files): tmpdir = tempfile.TemporaryDirectory() subprocess.run( ['git', 'clone', repository], capture_output=True, check=True, cwd=tmpdir.name) repository_name = os.listdir(tmpdir.name)[0] if not path: - path = repository_name - if os.path.exists(path): + path = Path(repository_name) + else: + path = Path(path) + if not allow_untracked_files and path.exists(): raise Exception(f'{path} exists') - os.makedirs(path) - tmpdir_repo = os.path.join(tmpdir.name, repository_name) + path.mkdir(exist_ok=allow_untracked_files) + tmpdir_repo = Path(tmpdir.name) / repository_name sha_process = subprocess.run( ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, cwd=tmpdir_repo) - with open(os.path.join(tmpdir_repo, '.beman_submodule'), 'w') as f: + with open(tmpdir_repo / '.beman_submodule', 'w') as f: f.write('[beman_submodule]\n') f.write(f'remote={repository}\n') f.write(f'commit_hash={sha_process.stdout.strip()}\n') - shutil.rmtree(os.path.join(tmpdir_repo, '.git')) + if allow_untracked_files: + f.write(f'allow_untracked_files=True\n') + shutil.rmtree(tmpdir_repo /'.git') shutil.copytree(tmpdir_repo, path, dirs_exist_ok=True) def status_command(paths): @@ -171,6 +216,9 @@ def get_parser(): parser_add.add_argument('repository', help='git repository to add') parser_add.add_argument( 'path', nargs='?', help='path where the repository will be added') + parser_add.add_argument( + '--allow-untracked-files', action='store_true', + help='the beman_submodule will not occupy the subdirectory exclusively') parser_status = subparsers.add_parser( 'status', help='show the status of beman_submodules') parser_status.add_argument('paths', nargs='*') @@ -186,7 +234,7 @@ def run_command(args): if args.command == 'update': update_command(args.remote, args.beman_submodule_path) elif args.command == 'add': - add_command(args.repository, args.path) + add_command(args.repository, args.path, args.allow_untracked_files) elif args.command == 'status': status_command(args.paths) else: diff --git a/infra/tools/beman-submodule/test/test_beman_submodule.py b/infra/tools/beman-submodule/test/test_beman_submodule.py index 47e2303..600fc07 100644 --- a/infra/tools/beman-submodule/test/test_beman_submodule.py +++ b/infra/tools/beman-submodule/test/test_beman_submodule.py @@ -1,27 +1,30 @@ # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +import glob import os -import pathlib import pytest import shutil import stat import subprocess import tempfile +from pathlib import Path # https://stackoverflow.com/a/19011259 import types import importlib.machinery loader = importlib.machinery.SourceFileLoader( 'beman_submodule', - os.path.join(os.path.dirname(os.path.realpath(__file__)), '../beman-submodule')) + str(Path(__file__).parent.resolve().parent / 'beman-submodule')) beman_submodule = types.ModuleType(loader.name) loader.exec_module(beman_submodule) def create_test_git_repository(): tmpdir = tempfile.TemporaryDirectory() + tmp_path = Path(tmpdir.name) + subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) def make_commit(a_txt_contents): - with open(os.path.join(tmpdir.name, 'a.txt'), 'w') as f: + with open(tmp_path / 'a.txt', 'w') as f: f.write(a_txt_contents) subprocess.run( ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) @@ -33,29 +36,84 @@ def make_commit(a_txt_contents): make_commit('a') return tmpdir +def create_test_git_repository2(): + tmpdir = tempfile.TemporaryDirectory() + tmp_path = Path(tmpdir.name) + + subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) + with open(tmp_path / 'a.txt', 'w') as f: + f.write('a') + subprocess.run( + ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + os.remove(tmp_path / 'a.txt') + subprocess.run( + ['git', 'rm', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) + with open(tmp_path / 'b.txt', 'w') as f: + f.write('b') + subprocess.run( + ['git', 'add', 'b.txt'], check=True, cwd=tmpdir.name, capture_output=True) + subprocess.run( + ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', + '--author="test "', '-m', 'test'], + check=True, cwd=tmpdir.name, capture_output=True) + return tmpdir + def test_directory_compare(): - def create_dir_structure(dir_path): - bar_path = os.path.join(dir_path, 'bar') + def create_dir_structure(dir_path: Path): + bar_path = dir_path / 'bar' os.makedirs(bar_path) - with open(os.path.join(dir_path, 'foo.txt'), 'w') as f: + with open(dir_path / 'foo.txt', 'w') as f: f.write('foo') - with open(os.path.join(bar_path, 'baz.txt'), 'w') as f: + with open(bar_path / 'baz.txt', 'w') as f: f.write('baz') with tempfile.TemporaryDirectory() as dir_a, \ tempfile.TemporaryDirectory() as dir_b: + path_a = Path(dir_a) + path_b = Path(dir_b) + + create_dir_structure(path_a) + create_dir_structure(path_b) + + assert beman_submodule.directory_compare(dir_a, dir_b, [], False) + + with open(path_a / 'bar' / 'quux.txt', 'w') as f: + f.write('quux') + + assert not beman_submodule.directory_compare(path_a, path_b, [], False) + assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], False) + +def test_directory_compare_untracked_files(): + def create_dir_structure(dir_path: Path): + bar_path = dir_path / 'bar' + os.makedirs(bar_path) + + with open(dir_path / 'foo.txt', 'w') as f: + f.write('foo') + with open(bar_path / 'baz.txt', 'w') as f: + f.write('baz') + + with tempfile.TemporaryDirectory() as reference, \ + tempfile.TemporaryDirectory() as actual: + path_a = Path(reference) + path_b = Path(actual) - create_dir_structure(dir_a) - create_dir_structure(dir_b) + create_dir_structure(path_a) + create_dir_structure(path_b) + (path_b / 'c.txt').touch() - assert beman_submodule.directory_compare(dir_a, dir_b, []) + assert beman_submodule.directory_compare(reference, actual, [], True) - with open(os.path.join(os.path.join(dir_a, 'bar'), 'quux.txt'), 'w') as f: + with open(path_a / 'bar' / 'quux.txt', 'w') as f: f.write('quux') - assert not beman_submodule.directory_compare(dir_a, dir_b, []) - assert beman_submodule.directory_compare(dir_a, dir_b, ['quux.txt']) + assert not beman_submodule.directory_compare(path_a, path_b, [], True) + assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], True) def test_parse_beman_submodule_file(): def valid_file(): @@ -67,7 +125,7 @@ def valid_file(): 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) tmpfile.flush() module = beman_submodule.parse_beman_submodule_file(tmpfile.name) - assert module.dirpath == str(pathlib.Path(tmpfile.name).resolve().parent) + assert module.dirpath == Path(tmpfile.name).resolve().parent assert module.remote == 'git@github.com:bemanproject/infra.git' assert module.commit_hash == '9b88395a86c4290794e503e94d8213b6c442ae77' valid_file() @@ -116,9 +174,9 @@ def invalid_file_wrong_section(): def test_get_beman_submodule(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo') + beman_submodule.add_command(tmpdir.name, 'foo', False) assert beman_submodule.get_beman_submodule('foo') os.remove('foo/.beman_submodule') assert not beman_submodule.get_beman_submodule('foo') @@ -127,25 +185,25 @@ def test_get_beman_submodule(): def test_find_beman_submodules_in(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo') - beman_submodule.add_command(tmpdir.name, 'bar') + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) beman_submodules = beman_submodule.find_beman_submodules_in(tmpdir2.name) sha_process = subprocess.run( ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, cwd=tmpdir.name) sha = sha_process.stdout.strip() - assert beman_submodules[0].dirpath == os.path.join(tmpdir2.name, 'bar') + assert beman_submodules[0].dirpath == Path(tmpdir2.name) / 'bar' assert beman_submodules[0].remote == tmpdir.name assert beman_submodules[0].commit_hash == sha - assert beman_submodules[1].dirpath == os.path.join(tmpdir2.name, 'foo') + assert beman_submodules[1].dirpath == Path(tmpdir2.name) / 'foo' assert beman_submodules[1].remote == tmpdir.name assert beman_submodules[1].commit_hash == sha os.chdir(original_cwd) def test_cwd_git_repository_path(): - original_cwd = os.getcwd() + original_cwd = Path.cwd() tmpdir = tempfile.TemporaryDirectory() os.chdir(tmpdir.name) assert not beman_submodule.cwd_git_repository_path() @@ -156,122 +214,229 @@ def test_cwd_git_repository_path(): def test_clone_beman_submodule_into_tmpdir(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(tmpdir2.name) sha_process = subprocess.run( ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, cwd=tmpdir.name) sha = sha_process.stdout.strip() - beman_submodule.add_command(tmpdir.name, 'foo') - module = beman_submodule.get_beman_submodule(os.path.join(tmpdir2.name, 'foo')) + beman_submodule.add_command(tmpdir.name, 'foo', False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') module.commit_hash = sha tmpdir3 = beman_submodule.clone_beman_submodule_into_tmpdir(module, False) - assert not beman_submodule.directory_compare(tmpdir.name, tmpdir3.name, ['.git']) + assert not beman_submodule.directory_compare( + tmpdir.name, tmpdir3.name, ['.git'], False) tmpdir4 = beman_submodule.clone_beman_submodule_into_tmpdir(module, True) - assert beman_submodule.directory_compare(tmpdir.name, tmpdir4.name, ['.git']) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir4.name, ['.git'], False) subprocess.run( ['git', 'reset', '--hard', sha], capture_output=True, check=True, cwd=tmpdir.name) - assert beman_submodule.directory_compare(tmpdir.name, tmpdir3.name, ['.git']) + assert beman_submodule.directory_compare(tmpdir.name, tmpdir3.name, ['.git'], False) + os.chdir(original_cwd) + +def test_get_paths(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') + assert beman_submodule.get_paths(module) == set(['a.txt']) os.chdir(original_cwd) def test_beman_submodule_status(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo') + beman_submodule.add_command(tmpdir.name, 'foo', False) sha_process = subprocess.run( ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, cwd=tmpdir.name) sha = sha_process.stdout.strip() assert ' ' + sha + ' foo' == beman_submodule.beman_submodule_status( - beman_submodule.get_beman_submodule(os.path.join(tmpdir2.name, 'foo'))) - with open(os.path.join(os.path.join(tmpdir2.name, 'foo'), 'a.txt'), 'w') as f: + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) + with open(Path(tmpdir2.name) / 'foo' / 'a.txt', 'w') as f: f.write('b') assert '+ ' + sha + ' foo' == beman_submodule.beman_submodule_status( - beman_submodule.get_beman_submodule(os.path.join(tmpdir2.name, 'foo'))) + beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) os.chdir(original_cwd) def test_update_command_no_paths(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo') - beman_submodule.add_command(tmpdir.name, 'bar') - sha_process = subprocess.run( + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, cwd=tmpdir.name) - sha = sha_process.stdout.strip() + parent_sha = parent_sha_process.stdout.strip() + parent_parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_parent_sha = parent_parent_sha_process.stdout.strip() subprocess.run( - ['git', 'reset', '--hard', sha], capture_output=True, check=True, + ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + beman_submodule.update_command(False, None) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + subprocess.run( + ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, cwd=tmpdir.name) - with open(os.path.join(os.path.join(tmpdir2.name, 'foo'), '.beman_submodule'), 'w') as f: - f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n') - with open(os.path.join(os.path.join(tmpdir2.name, 'bar'), '.beman_submodule'), 'w') as f: - f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n') - beman_submodule.update_command(tmpdir.name, None) + beman_submodule.update_command(True, None) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' assert beman_submodule.directory_compare( - tmpdir.name, os.path.join(tmpdir2.name, 'foo'), ['.git', '.beman_submodule']) + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) assert beman_submodule.directory_compare( - tmpdir.name, os.path.join(tmpdir2.name, 'bar'), ['.git', '.beman_submodule']) + tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) os.chdir(original_cwd) def test_update_command_with_path(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() - tmpdir_copy1 = tempfile.TemporaryDirectory() - shutil.copytree(tmpdir.name, tmpdir_copy1.name, dirs_exist_ok=True) - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo') - beman_submodule.add_command(tmpdir.name, 'bar') - sha_process = subprocess.run( + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, cwd=tmpdir.name) - sha = sha_process.stdout.strip() + parent_sha = parent_sha_process.stdout.strip() + parent_parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_parent_sha = parent_parent_sha_process.stdout.strip() subprocess.run( - ['git', 'reset', '--hard', sha], capture_output=True, check=True, + ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, cwd=tmpdir.name) - with open(os.path.join(os.path.join(tmpdir2.name, 'foo'), '.beman_submodule'), 'w') as f: - f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n') - with open(os.path.join(os.path.join(tmpdir2.name, 'bar'), '.beman_submodule'), 'w') as f: - f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n') - beman_submodule.update_command(tmpdir.name, 'foo') + tmpdir_parent_parent_copy = tempfile.TemporaryDirectory() + shutil.copytree(tmpdir.name, tmpdir_parent_parent_copy.name, dirs_exist_ok=True) + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') + beman_submodule.update_command(False, 'foo') + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + subprocess.run( + ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, + cwd=tmpdir.name) + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) assert beman_submodule.directory_compare( - tmpdir.name, os.path.join(tmpdir2.name, 'foo'), ['.git', '.beman_submodule']) + tmpdir_parent_parent_copy.name, + Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + subprocess.run( + ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, + cwd=tmpdir.name) + beman_submodule.update_command(True, 'foo') + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' + with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: + assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' + assert beman_submodule.directory_compare( + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) assert beman_submodule.directory_compare( - tmpdir_copy1.name, os.path.join(tmpdir2.name, 'bar'), ['.git', '.beman_submodule']) + tmpdir_parent_parent_copy.name, + Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) + os.chdir(original_cwd) + +def test_update_command_untracked_files(): + tmpdir = create_test_git_repository2() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd(); + os.chdir(tmpdir2.name) + orig_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + orig_sha = orig_sha_process.stdout.strip() + parent_sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + parent_sha = parent_sha_process.stdout.strip() + os.makedirs(Path(tmpdir2.name) / 'foo') + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: + f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\nallow_untracked_files=True') + beman_submodule.update_command(False, 'foo') + assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + beman_submodule.update_command(True, 'foo') + assert set(['./foo/b.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) os.chdir(original_cwd) def test_add_command(): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo') + beman_submodule.add_command(tmpdir.name, 'foo', False) sha_process = subprocess.run( ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, cwd=tmpdir.name) sha = sha_process.stdout.strip() assert beman_submodule.directory_compare( - tmpdir.name, os.path.join(tmpdir2.name, 'foo'), ['.git', '.beman_submodule']) - with open(os.path.join(os.path.join(tmpdir2.name, 'foo'), '.beman_submodule'), 'r') as f: + tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) + with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n' os.chdir(original_cwd) +def test_add_command_untracked_files(): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + os.makedirs(Path(tmpdir2.name) / 'foo') + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + beman_submodule.add_command(tmpdir.name, 'foo', True) + assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) + os.chdir(original_cwd) + def test_status_command_no_paths(capsys): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo') - beman_submodule.add_command(tmpdir.name, 'bar') + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) sha_process = subprocess.run( ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, cwd=tmpdir.name) - with open(os.path.join(os.path.join(tmpdir2.name, 'bar'), 'a.txt'), 'w') as f: + with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: f.write('b') beman_submodule.status_command([]) sha = sha_process.stdout.strip() @@ -281,24 +446,39 @@ def test_status_command_no_paths(capsys): def test_status_command_with_path(capsys): tmpdir = create_test_git_repository() tmpdir2 = create_test_git_repository() - original_cwd = os.getcwd() + original_cwd = Path.cwd() os.chdir(tmpdir2.name) - beman_submodule.add_command(tmpdir.name, 'foo') - beman_submodule.add_command(tmpdir.name, 'bar') + beman_submodule.add_command(tmpdir.name, 'foo', False) + beman_submodule.add_command(tmpdir.name, 'bar', False) sha_process = subprocess.run( ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, cwd=tmpdir.name) - with open(os.path.join(os.path.join(tmpdir2.name, 'bar'), 'a.txt'), 'w') as f: + with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: f.write('b') beman_submodule.status_command(['bar']) sha = sha_process.stdout.strip() assert capsys.readouterr().out == '+ ' + sha + ' bar\n' os.chdir(original_cwd) +def test_status_command_untracked_files(capsys): + tmpdir = create_test_git_repository() + tmpdir2 = create_test_git_repository() + original_cwd = Path.cwd() + os.chdir(tmpdir2.name) + beman_submodule.add_command(tmpdir.name, 'foo', True) + sha_process = subprocess.run( + ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, + cwd=tmpdir.name) + (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() + beman_submodule.status_command(['foo']) + sha = sha_process.stdout.strip() + assert capsys.readouterr().out == ' ' + sha + ' foo\n' + os.chdir(original_cwd) + def test_check_for_git(): tmpdir = tempfile.TemporaryDirectory() assert not beman_submodule.check_for_git(tmpdir.name) - fake_git_path = os.path.join(tmpdir.name, 'git') + fake_git_path = Path(tmpdir.name) / 'git' with open(fake_git_path, 'w'): pass os.chmod(fake_git_path, stat.S_IRWXU) diff --git a/src/beman/cstring_view/CMakeLists.txt b/src/beman/cstring_view/CMakeLists.txt index ec6984b..350759f 100644 --- a/src/beman/cstring_view/CMakeLists.txt +++ b/src/beman/cstring_view/CMakeLists.txt @@ -11,9 +11,9 @@ target_sources( beman.cstring_view PUBLIC FILE_SET HEADERS - BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/../../../include - FILES - ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/beman/cstring_view/cstring_view.hpp + BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/../../../include + FILES + ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/beman/cstring_view/cstring_view.hpp ) set_target_properties( @@ -22,10 +22,10 @@ set_target_properties( ) install( - TARGETS beman.cstring_view COMPONENT beman.cstring_view + TARGETS beman.cstring_view + COMPONENT beman.cstring_view EXPORT beman.cstring_view - DESTINATION - ${CMAKE_INSTALL_LIBDIR}$<$:/debug> + DESTINATION ${CMAKE_INSTALL_LIBDIR}$<$:/debug> RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}$<$:/debug> FILE_SET HEADERS DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} )