Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ body:
attributes:
label: Environment
description: OS, compiler, build preset, and version/tag
placeholder: macOS 15, AppleClang 17, vcpkg-debug, v0.1.0
placeholder: macOS 15, AppleClang 17, vcpkg-debug, v0.2.1
validations:
required: true

Expand Down
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,19 @@ jobs:
if: runner.os != 'Windows'
run: ccache --show-stats

- name: Upload platform acceptance evidence on failure
if: failure()
uses: actions/upload-artifact@v6
with:
name: platform-acceptance-${{ matrix.os }}
path: |
build/vcpkg-debug/platform-acceptance-*/**/report.json
build/vcpkg-debug/platform-acceptance-*/**/*.stdout
build/vcpkg-debug/platform-acceptance-*/**/*.stderr
build/vcpkg-debug/platform-acceptance-*/**/*.command.json
retention-days: 7
if-no-files-found: ignore

sanitizers:
name: Sanitizers (ubuntu-latest)
runs-on: ubuntu-latest
Expand Down
15 changes: 12 additions & 3 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -428,14 +428,15 @@ if (NUMTILO_BUILD_APP)
if (APPLE AND NUMTILO_MACOS_BUNDLE)
install(TARGETS numtilo BUNDLE DESTINATION . COMPONENT Runtime)
else()
include(cmake/RuntimeDependencies.cmake)
install(TARGETS numtilo
RUNTIME_DEPENDENCY_SET numtilo_runtime_deps
RUNTIME DESTINATION . COMPONENT Runtime
)
install(RUNTIME_DEPENDENCY_SET numtilo_runtime_deps
PRE_EXCLUDE_REGEXES "api-ms-" "ext-ms-"
"$<$<PLATFORM_ID:Windows>:^hvsifiletrust\\.dll$>"
POST_EXCLUDE_REGEXES ".*/[Ss][Yy][Ss][Tt][Ee][Mm]32/.*\\.dll" ".*/usr/lib/.*" ".*/System/Library/.*"
PRE_EXCLUDE_REGEXES ${NUMTILO_RUNTIME_PRE_EXCLUDE_REGEXES}
POST_INCLUDE_REGEXES ${NUMTILO_RUNTIME_POST_INCLUDE_REGEXES}
POST_EXCLUDE_REGEXES ${NUMTILO_RUNTIME_POST_EXCLUDE_REGEXES}
RUNTIME DESTINATION . COMPONENT Runtime
LIBRARY DESTINATION . COMPONENT Runtime
)
Expand Down Expand Up @@ -554,6 +555,14 @@ if (BUILD_TESTING)

find_package(Python3 COMPONENTS Interpreter QUIET)
if (Python3_Interpreter_FOUND AND NOT CMAKE_CROSSCOMPILING)
add_test(
NAME runtime_dependency_controls
COMMAND "${Python3_EXECUTABLE}" -B -m unittest discover
-s "${CMAKE_CURRENT_SOURCE_DIR}/scripts/tests"
-p test_runtime_dependencies.py -v
)
set_tests_properties(runtime_dependency_controls PROPERTIES
LABELS "packaging" TIMEOUT 60)
add_test(
NAME platform_acceptance
COMMAND "${Python3_EXECUTABLE}" "${CMAKE_CURRENT_SOURCE_DIR}/scripts/verify_platform.py"
Expand Down
8 changes: 3 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

## Workflow

The new Numtilo GitHub repository is pending creation. GitHub fork/PR and branch-protection
steps below apply once its actual repository address is available.
The project repository is [EnesGoker/Numtilo](https://github.com/EnesGoker/Numtilo).

1. Fork/branch from `main`.
2. Make a focused change.
Expand Down Expand Up @@ -60,11 +59,10 @@ ctest --test-dir build/vcpkg-debug -C Debug --output-on-failure
Apply branch protection rules for `main` (required checks + review gate):

```bash
./scripts/apply_branch_protection.sh OWNER/REPO
./scripts/apply_branch_protection.sh EnesGoker/Numtilo
```

Replace `OWNER/REPO` with the verified repository after creation. If `origin` points to
that GitHub repository, the repo argument can be omitted:
If `origin` points to this GitHub repository, the repo argument can be omitted:

```bash
./scripts/apply_branch_protection.sh
Expand Down
18 changes: 14 additions & 4 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,28 @@ Security fixes are provided for the latest `main` branch and the most recent tag

## Reporting A Vulnerability

Please do not open public issues for suspected vulnerabilities.
Do not post vulnerability details or sensitive attachments in public issues.

Report privately with:
If **Report a vulnerability** is available on the repository's
[Security advisories page](https://github.com/EnesGoker/Numtilo/security/advisories),
submit your report there privately.

If that option is unavailable, use the [bug report form](https://github.com/EnesGoker/Numtilo/issues/new?template=bug_report.yml)
only to ask the maintainer to enable GitHub private vulnerability reporting.
In required form fields, enter neutral text such as `Private reporting channel requested`
instead of vulnerability information. Do not include vulnerability details, reproduction
steps, proof-of-concept, credentials or attachments in the public form.
Wait until **Report a vulnerability** is available, then submit sensitive information
only through that private option.

In the private report, include:

- affected version/commit
- platform (macOS/Linux/Windows)
- reproduction steps
- impact assessment
- optional proof-of-concept

Until a dedicated security mailbox is added, open a private channel with the repository owner and reference this file.

## Response Targets

- Initial acknowledgment: within 7 days
Expand Down
179 changes: 179 additions & 0 deletions cmake/RuntimeDependencies.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Keep platform libraries out of flat Runtime packages without dropping the
# application's shared third-party dependencies.
cmake_minimum_required(VERSION 3.24)
include_guard(GLOBAL)

if (CMAKE_SYSTEM_NAME)
set(NUMTILO_RUNTIME_SYSTEM_NAME "${CMAKE_SYSTEM_NAME}")
else()
set(NUMTILO_RUNTIME_SYSTEM_NAME "${CMAKE_HOST_SYSTEM_NAME}")
endif()

set(NUMTILO_RUNTIME_PRE_EXCLUDE_REGEXES "")
set(NUMTILO_RUNTIME_POST_EXCLUDE_REGEXES "")
set(NUMTILO_RUNTIME_POST_INCLUDE_REGEXES "")
set(NUMTILO_RUNTIME_FORBIDDEN_NAMES "")

if (NUMTILO_RUNTIME_SYSTEM_NAME STREQUAL "Windows")
# CMake lowercases DLL names for PRE matching. These are Windows components,
# not the redistributable MSVC runtime or third-party graphics/audio DLLs.
set(NUMTILO_RUNTIME_FORBIDDEN_NAMES
"^(advapi32|avrt|bcrypt|combase|crypt32|dwmapi|gdi32|imm32|kernel32|kernelbase|msvcp_win|ntdll|ole32|oleaut32|opengl32|rpcrt4|secur32|setupapi|shell32|shlwapi|ucrtbase|user32|version|winmm|winspool|ws2_32)[.]dll$"
"^(api-ms-|ext-ms-).*"
"^hvsifiletrust[.]dll$"
)
set(NUMTILO_RUNTIME_PRE_EXCLUDE_REGEXES ${NUMTILO_RUNTIME_FORBIDDEN_NAMES})
# Windows APIs can return backslashes while CMake appends a forward slash.
set(NUMTILO_RUNTIME_POST_EXCLUDE_REGEXES
".*[/\\][Ss][Yy][Ss][Tt][Ee][Mm]32[/\\].*[.][Dd][Ll][Ll]$"
".*[/\\][Ss][Yy][Ss][Ww][Oo][Ww]64[/\\].*[.][Dd][Ll][Ll]$"
)
# These redistributable runtimes may resolve from System32 on build hosts.
# Include them explicitly so the installed application remains relocatable.
set(NUMTILO_RUNTIME_POST_INCLUDE_REGEXES
".*[/\\]([Mm][Ss][Vv][Cc][Pp]|[Vv][Cc][Rr][Uu][Nn][Tt][Ii][Mm][Ee]|[Cc][Oo][Nn][Cc][Rr][Tt]|[Vv][Cc][Oo][Mm][Pp])[0-9]+(_[A-Za-z0-9_]+)?[.][Dd][Ll][Ll]$"
".*[/\\][Ss][Ff][Mm][Ll]-([Gg][Rr][Aa][Pp][Hh][Ii][Cc][Ss]|[Ww][Ii][Nn][Dd][Oo][Ww]|[Ss][Yy][Ss][Tt][Ee][Mm]|[Aa][Uu][Dd][Ii][Oo])(-[Dd])?(-[0-9]+)?[.][Dd][Ll][Ll]$"
".*[/\\][Oo][Pp][Ee][Nn][Aa][Ll]32[.][Dd][Ll][Ll]$"
)
elseif (NUMTILO_RUNTIME_SYSTEM_NAME STREQUAL "Linux")
# glibc and its ELF loader belong to the target OS. Do not exclude /usr/lib
# wholesale: it also supplies OpenAL, codecs, graphics and C++ runtimes.
set(NUMTILO_RUNTIME_FORBIDDEN_NAMES
"^lib(c|m|mvec|dl|pthread|rt|util|resolv|anl|BrokenLocale|nss_(compat|dns|files|hesiod))[.]so([.][0-9]+)*$"
"^ld-linux(-[A-Za-z0-9_-]+)?[.]so([.][0-9]+)*$"
"^ld64[.]so([.][0-9]+)*$"
"^(lib(c|m|mvec|dl|pthread|rt|util|resolv|anl|BrokenLocale|nss_(compat|dns|files|hesiod))|ld)-[0-9]+([.][0-9]+)*[.]so$"
)
set(NUMTILO_RUNTIME_PRE_EXCLUDE_REGEXES ${NUMTILO_RUNTIME_FORBIDDEN_NAMES})
elseif (NUMTILO_RUNTIME_SYSTEM_NAME STREQUAL "Darwin")
set(NUMTILO_RUNTIME_POST_EXCLUDE_REGEXES ".*/usr/lib/.*" ".*/System/Library/.*")
endif()

function(numtilo_verify_runtime_dependencies executable)
# macOS bundles retain their separate BundleUtilities verification path.
if (NOT NUMTILO_RUNTIME_SYSTEM_NAME MATCHES "^(Windows|Linux)$")
message(FATAL_ERROR "The flat Runtime dependency check requires Windows or Linux")
endif()
if (NOT EXISTS "${executable}" OR IS_DIRECTORY "${executable}")
message(FATAL_ERROR "Runtime executable does not exist: ${executable}")
endif()

file(REAL_PATH "${executable}" _numtilo_executable)
get_filename_component(_numtilo_root "${_numtilo_executable}" DIRECTORY)
file(GLOB_RECURSE _numtilo_members LIST_DIRECTORIES FALSE "${_numtilo_root}/*")
foreach(_numtilo_member IN LISTS _numtilo_members)
get_filename_component(_numtilo_name "${_numtilo_member}" NAME)
if (NUMTILO_RUNTIME_SYSTEM_NAME STREQUAL "Windows")
string(TOLOWER "${_numtilo_name}" _numtilo_name)
endif()
foreach(_numtilo_forbidden IN LISTS NUMTILO_RUNTIME_FORBIDDEN_NAMES)
if (_numtilo_name MATCHES "${_numtilo_forbidden}")
message(FATAL_ERROR "OS runtime library must not be packaged: ${_numtilo_member}")
endif()
endforeach()
endforeach()

file(GET_RUNTIME_DEPENDENCIES
EXECUTABLES "${_numtilo_executable}"
DIRECTORIES "${_numtilo_root}"
RESOLVED_DEPENDENCIES_VAR _numtilo_resolved
UNRESOLVED_DEPENDENCIES_VAR _numtilo_unresolved
CONFLICTING_DEPENDENCIES_PREFIX _numtilo_conflicts
PRE_EXCLUDE_REGEXES ${NUMTILO_RUNTIME_PRE_EXCLUDE_REGEXES}
POST_EXCLUDE_REGEXES ${NUMTILO_RUNTIME_POST_EXCLUDE_REGEXES}
POST_INCLUDE_REGEXES ${NUMTILO_RUNTIME_POST_INCLUDE_REGEXES}
)
if (_numtilo_unresolved)
message(FATAL_ERROR "Unresolved Runtime dependencies: ${_numtilo_unresolved}")
endif()
if (_numtilo_conflicts_FILENAMES AND NOT NUMTILO_RUNTIME_SYSTEM_NAME STREQUAL "Linux")
foreach(_numtilo_name IN LISTS _numtilo_conflicts_FILENAMES)
message(STATUS "Conflicting Runtime library ${_numtilo_name}: ${_numtilo_conflicts_${_numtilo_name}}")
endforeach()
message(FATAL_ERROR "Conflicting Runtime dependencies: ${_numtilo_conflicts_FILENAMES}")
endif()

# Linux RUNPATH is not inherited by transitive dependencies, so the scanner
# can report an OS search path even when the same library is packaged. Check
# every resolved non-OS library's companion in this flat Runtime package.
set(_numtilo_required ${_numtilo_resolved})
if (NUMTILO_RUNTIME_SYSTEM_NAME STREQUAL "Linux")
# CMake removes conflicting names from RESOLVED_DEPENDENCIES_VAR. Their
# packaged companions must pass the same checks before any hash reads.
foreach(_numtilo_name IN LISTS _numtilo_conflicts_FILENAMES)
list(APPEND _numtilo_required "${_numtilo_root}/${_numtilo_name}")
endforeach()
endif()
list(REMOVE_DUPLICATES _numtilo_required)
set(_numtilo_companions "")
foreach(_numtilo_dependency IN LISTS _numtilo_required)
get_filename_component(_numtilo_name "${_numtilo_dependency}" NAME)
set(_numtilo_companion "${_numtilo_root}/${_numtilo_name}")
if (NOT EXISTS "${_numtilo_companion}" OR IS_DIRECTORY "${_numtilo_companion}")
message(FATAL_ERROR "Required Runtime library is missing from the package: ${_numtilo_name}")
endif()
list(APPEND _numtilo_companions "${_numtilo_companion}")
endforeach()
# CMake 3.29's Windows REAL_PATH only expands the absolute lexical path.
# Python resolves native file links and parent junctions on every host,
# including when a portable fixture selects a different target platform.
if (NOT Python3_EXECUTABLE)
find_package(Python3 3.8 COMPONENTS Interpreter REQUIRED)
endif()
execute_process(
COMMAND "${Python3_EXECUTABLE}" -B -c [=[
from pathlib import Path
import sys

try:
root = Path(sys.argv[1]).resolve(strict=True)
for name in sys.argv[2:]:
actual = Path(name).resolve(strict=True)
if not actual.is_file():
raise ValueError("Required runtime library is not a regular file: " + name)
actual.relative_to(root)
except (OSError, RuntimeError, ValueError) as error:
print(str(error), file=sys.stderr)
sys.exit(1)
]=] "${_numtilo_root}" ${_numtilo_companions}
RESULT_VARIABLE _numtilo_containment_result
ERROR_VARIABLE _numtilo_containment_error
TIMEOUT 15
)
if (NOT _numtilo_containment_result STREQUAL "0")
message(FATAL_ERROR "Runtime library points outside the package or cannot be resolved safely: ${_numtilo_containment_error}")
endif()

# A copied library and its host original have different filesystem identities
# even when identical. Only Linux permits this case, after all companions are
# proven to stay inside the package and every conflicting copy matches bytes.
if (NUMTILO_RUNTIME_SYSTEM_NAME STREQUAL "Linux")
foreach(_numtilo_name IN LISTS _numtilo_conflicts_FILENAMES)
set(_numtilo_candidates "${_numtilo_conflicts_${_numtilo_name}}")
list(LENGTH _numtilo_candidates _numtilo_candidate_count)
if (_numtilo_candidate_count LESS 2)
message(FATAL_ERROR "Conflicting Runtime dependencies have missing candidate paths: ${_numtilo_name}")
endif()
file(SHA256 "${_numtilo_root}/${_numtilo_name}" _numtilo_companion_sha256)
foreach(_numtilo_candidate IN LISTS _numtilo_candidates)
if (NOT EXISTS "${_numtilo_candidate}" OR IS_DIRECTORY "${_numtilo_candidate}")
message(FATAL_ERROR "Conflicting Runtime dependency candidate is missing: ${_numtilo_candidate}")
endif()
file(SHA256 "${_numtilo_candidate}" _numtilo_candidate_sha256)
if (NOT _numtilo_candidate_sha256 STREQUAL _numtilo_companion_sha256)
message(FATAL_ERROR "Conflicting Runtime dependencies differ in content: ${_numtilo_name}")
endif()
endforeach()
message(STATUS "Hash-verified identical Linux Runtime copies: ${_numtilo_name}")
endforeach()
endif()
list(LENGTH _numtilo_required _numtilo_count)
message(STATUS "Verified ${_numtilo_count} packaged Runtime dependencies (${NUMTILO_RUNTIME_SYSTEM_NAME})")
endfunction()

if (CMAKE_SCRIPT_MODE_FILE AND CMAKE_SCRIPT_MODE_FILE STREQUAL CMAKE_CURRENT_LIST_FILE)
if (NOT NUMTILO_RUNTIME_EXECUTABLE)
message(FATAL_ERROR "NUMTILO_RUNTIME_EXECUTABLE is required")
endif()
numtilo_verify_runtime_dependencies("${NUMTILO_RUNTIME_EXECUTABLE}")
endif()
1 change: 1 addition & 0 deletions scripts/stage_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ class StageError(Exception):
"cmake/StageSourcePackage.cmake.in", "cmake/MacOSXBundleInfo.plist.in",
"cmake/CleanInstalledAssets.cmake.in", "cmake/FixupMacBundle.cmake.in",
"cmake/SyncAssets.cmake", "cmake/VerifyHeadlessDependencies.cmake",
"cmake/RuntimeDependencies.cmake",
"assets/fonts/Inter-Variable.ttf", "assets/fonts/LICENSE-Inter.txt",
"assets/sounds/slide.wav", "assets/sounds/merge.wav",
"assets/sounds/spawn.wav", "assets/sounds/game_over.wav",
Expand Down
Loading
Loading