Skip to content

Commit f5d1cbf

Browse files
Merge pull request ExplosionEngine#418 from FlyAndNotDown/master
Feat: Misc Update
2 parents fcd3847 + cb45162 commit f5d1cbf

6 files changed

Lines changed: 173 additions & 11 deletions

File tree

.github/workflows/build.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ jobs:
4646
with:
4747
node-version: 24
4848

49+
- name: Setup Python
50+
uses: actions/setup-python@v5
51+
with:
52+
python-version: '3.12'
53+
4954
- name: Setup CMake
5055
uses: jwlawson/actions-setup-cmake@v2
5156
with:
@@ -57,6 +62,14 @@ jobs:
5762
- name: Config Conan Remote
5863
run: conan remote add explosion https://conan.kindem.online/artifactory/api/conan/conan
5964

65+
# Register the in-repo recipes in the local cache so the engine's 'conan install --build=missing'
66+
# resolves them from this checkout: unchanged recipes hash to the revision already published on the
67+
# remote (binaries are downloaded), changed ones get built locally, making recipe PRs self-contained.
68+
- name: Export Conan Recipes
69+
run: |
70+
pip install pyyaml
71+
python ThirdParty/ConanRecipes/build_recipes.py --export-only
72+
6073
- name: Configure CMake
6174
run: cmake -B ${{github.workspace}}/build -G=Ninja -DCMAKE_BUILD_TYPE=${{env.BUILD_TYPE}} -DCI=ON
6275

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
name: Publish Conan Recipes
2+
3+
on:
4+
push:
5+
branches: [master]
6+
paths:
7+
- 'ThirdParty/ConanRecipes/**'
8+
- '.github/workflows/publish-conan-recipes.yml'
9+
workflow_dispatch:
10+
11+
concurrency:
12+
group: publish-conan-recipes
13+
14+
env:
15+
CONAN_REMOTE_NAME: explosion
16+
CONAN_REMOTE_URL: https://conan.kindem.online/artifactory/api/conan/conan
17+
CMAKE_VERSION: '4.1.2'
18+
19+
jobs:
20+
publish:
21+
# Guard against forks: pushes to a fork's master must not run (and could not upload anyway,
22+
# since secrets are only configured on the canonical repository).
23+
if: github.repository == 'ExplosionEngine/Explosion'
24+
25+
strategy:
26+
# Platforms publish independently: a failure on one OS must not cancel the other mid-upload.
27+
fail-fast: false
28+
matrix:
29+
# cppstd is pinned per platform to match the binaries already on the remote; consumers on a
30+
# higher standard (the engine uses C++20) still match via Conan's default compatibility plugin.
31+
include:
32+
- os: windows-latest
33+
cppstd: '17'
34+
- os: macOS-latest
35+
cppstd: gnu17
36+
37+
runs-on: ${{ matrix.os }}
38+
39+
steps:
40+
- name: Set XCode Version
41+
run: sudo xcode-select -s /Applications/Xcode.app/Contents/Developer
42+
if: runner.os == 'macOS'
43+
44+
- name: Setup MSVC
45+
uses: ilammy/msvc-dev-cmd@v1
46+
if: runner.os == 'Windows'
47+
48+
- name: Checkout Repo
49+
uses: actions/checkout@v4
50+
51+
- name: Setup Python
52+
uses: actions/setup-python@v5
53+
with:
54+
python-version: '3.12'
55+
56+
- name: Setup CMake
57+
uses: jwlawson/actions-setup-cmake@v2
58+
with:
59+
cmake-version: ${{env.CMAKE_VERSION}}
60+
61+
- name: Setup Conan
62+
uses: conan-io/setup-conan@v1
63+
64+
- name: Detect Conan Profile
65+
run: conan profile detect --force
66+
67+
- name: Config Conan Remote
68+
run: conan remote add ${{ env.CONAN_REMOTE_NAME }} ${{ env.CONAN_REMOTE_URL }}
69+
70+
- name: Install Python Dependencies
71+
run: pip install pyyaml
72+
73+
# --build=missing (the script default) downloads binaries the remote already has for the same
74+
# recipe revision, so only recipes actually changed by the triggering push get rebuilt.
75+
- name: Build And Upload Recipes
76+
run: >-
77+
python ThirdParty/ConanRecipes/build_recipes.py
78+
--conan-arg=--settings=compiler.cppstd=${{ matrix.cppstd }}
79+
--upload
80+
--remote ${{ env.CONAN_REMOTE_NAME }}
81+
--remote-user ${{ secrets.CONAN_REMOTE_USER }}
82+
--remote-password ${{ secrets.CONAN_REMOTE_PASSWORD }}

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,7 @@ TestProject/.idea
2828
TestProject/.vscode
2929
TestProject/cmake-build*
3030
TestProject/build*
31+
32+
# macOS
33+
.DS_Store
34+
**/.DS_Store

CMake/Common.cmake

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,9 @@ if (${MSVC})
2929
NOMINMAX=1
3030
)
3131
endif ()
32+
33+
# This project and its downstream consumers reference Qt only through the versioned Qt6:: targets. Suppressing the
34+
# versionless Qt:: aliases stops the single-config Qt host tools from triggering "IMPORTED_LOCATION not set ...
35+
# configuration <cfg>" errors in the IDE file-API codemodel on non-Release builds, where they only carry a Release
36+
# import while Debug/RelWithDebInfo/MinSizeRel are mapped onto it.
37+
set(QT_NO_CREATE_VERSIONLESS_TARGETS TRUE)

ThirdParty/ConanRecipes/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,4 +45,28 @@ python build_recipes.py --upload \
4545
--remote <remote> \
4646
--remote-url <remote-url> \
4747
--remote-user <user> --remote-password <password>
48+
49+
# just register every recipe version in the local conan cache (no build);
50+
# a later 'conan install --build=missing' then builds whatever the remote
51+
# has no binaries for
52+
python build_recipes.py --export-only
4853
```
54+
55+
## CI
56+
57+
Recipe changes are validated and published automatically:
58+
59+
- Every pull request runs the engine build workflow, which first runs
60+
`build_recipes.py --export-only`. Unchanged recipes hash to the same
61+
revision already published on the remote so their binaries are simply
62+
downloaded, while recipes changed by the PR are built from source inside
63+
the job. A PR can therefore change recipes and engine code together and
64+
be validated atomically.
65+
- After a push to `master` that touches `ThirdParty/ConanRecipes`, the
66+
`Publish Conan Recipes` workflow builds the changed recipes on Windows
67+
and macOS and uploads them to the remote (credentials come from the
68+
`CONAN_REMOTE_USER` / `CONAN_REMOTE_PASSWORD` repository secrets). It can
69+
also be re-run manually via `workflow_dispatch` if an upload failed.
70+
71+
To keep remote revisions in sync with git, avoid uploading from local
72+
machines; let the publish workflow be the only writer.

ThirdParty/ConanRecipes/build_recipes.py

Lines changed: 44 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@
55
top entry in conandata.yml), filtered by the recipe's `platforms` list. The
66
first failure stops the run and prints a summary. Uploading is opt-in and only
77
runs once every recipe has built successfully.
8+
9+
With --export-only the script just runs 'conan export' for every version of
10+
every recipe, so a subsequent 'conan install --build=missing' resolves recipes
11+
from the local cache and builds whatever the remote has no binaries for. CI
12+
uses this to validate recipe changes together with the engine build.
813
"""
914

1015
from __future__ import annotations
@@ -45,7 +50,8 @@ def __init__(self, path: Path):
4550

4651
data = yaml.safe_load(self.conandata.read_text(encoding="utf-8")) or {}
4752
self.name = self._parse_name() or self.dir_name
48-
self.version = latest_version(data)
53+
self.versions = list_versions(data)
54+
self.version = self.versions[0] if self.versions else None
4955
self.platforms = data.get("platforms") or []
5056
self.requires = data.get("requires") or []
5157

@@ -62,14 +68,14 @@ def reference(self) -> str:
6268
return f"{self.name}/{self.version}"
6369

6470

65-
def latest_version(data: dict) -> str | None:
71+
def list_versions(data: dict) -> list[str]:
6672
sources = data.get("sources")
6773
if isinstance(sources, dict) and sources:
68-
return next(iter(sources))
74+
return [str(v) for v in sources]
6975
versions = data.get("versions")
7076
if isinstance(versions, list) and versions:
71-
return versions[0]
72-
return None
77+
return [str(v) for v in versions]
78+
return []
7379

7480

7581
def discover_recipes(root: Path) -> list[Recipe]:
@@ -104,8 +110,9 @@ def visit(recipe: Recipe):
104110
return ordered
105111

106112

107-
def run(cmd: list[str], cwd: Path | None = None) -> int:
108-
print(f"\n$ {' '.join(cmd)}", flush=True)
113+
def run(cmd: list[str], cwd: Path | None = None, redact: set[str] | None = None) -> int:
114+
shown = " ".join("***" if redact and arg in redact else arg for arg in cmd)
115+
print(f"\n$ {shown}", flush=True)
109116
return subprocess.run(cmd, cwd=str(cwd) if cwd else None).returncode
110117

111118

@@ -140,6 +147,11 @@ def parse_args() -> argparse.Namespace:
140147
parser.add_argument(
141148
"--skip", action="append", default=[], help="skip these recipe names (repeatable)"
142149
)
150+
parser.add_argument(
151+
"--export-only",
152+
action="store_true",
153+
help="only 'conan export' every version of every recipe; no build, no platform filter",
154+
)
143155

144156
upload = parser.add_argument_group("upload")
145157
upload.add_argument(
@@ -157,9 +169,24 @@ def parse_args() -> argparse.Namespace:
157169
args = parser.parse_args()
158170
if args.upload and not args.remote:
159171
parser.error("--upload requires --remote")
172+
if args.export_only and args.upload:
173+
parser.error("--export-only cannot be combined with --upload")
160174
return args
161175

162176

177+
def export_all(args: argparse.Namespace, recipes: list[Recipe]):
178+
count = 0
179+
for recipe in recipes:
180+
if not recipe.versions:
181+
sys.exit(f"error: could not determine versions from {recipe.conandata}")
182+
for version in recipe.versions:
183+
cmd = [args.conan, "export", f"{recipe.dir_name}/conanfile.py", "--version", version]
184+
if run(cmd, cwd=args.recipes_root) != 0:
185+
sys.exit(f"error: failed to export {recipe.name}/{version}")
186+
count += 1
187+
print(f"\nExported {count} recipe version(s).", flush=True)
188+
189+
163190
def build_all(args: argparse.Namespace, recipes: list[Recipe], host: str):
164191
built: list[Recipe] = []
165192
skipped: list[tuple[Recipe, str]] = []
@@ -215,9 +242,11 @@ def upload_all(args: argparse.Namespace, built: list[Recipe]):
215242

216243
if args.remote_user is not None:
217244
login = [args.conan, "remote", "login", args.remote, args.remote_user]
245+
redact: set[str] = set()
218246
if args.remote_password is not None:
219247
login += ["-p", args.remote_password]
220-
if run(login) != 0:
248+
redact.add(args.remote_password)
249+
if run(login, redact=redact) != 0:
221250
sys.exit("error: failed to log in to remote")
222251

223252
for recipe in built:
@@ -250,9 +279,6 @@ def main():
250279
if not root.is_dir():
251280
sys.exit(f"error: recipes root not found: {root}")
252281

253-
host = current_platform()
254-
print(f"Host platform: {host}", flush=True)
255-
256282
recipes = discover_recipes(root)
257283
if args.only:
258284
recipes = [r for r in recipes if r.name in args.only or r.dir_name in args.only]
@@ -261,6 +287,13 @@ def main():
261287
if not recipes:
262288
sys.exit("error: no recipes to build")
263289

290+
if args.export_only:
291+
export_all(args, recipes)
292+
return
293+
294+
host = current_platform()
295+
print(f"Host platform: {host}", flush=True)
296+
264297
recipes = order_by_dependencies(recipes)
265298
print("Build order: " + ", ".join(r.name for r in recipes), flush=True)
266299

0 commit comments

Comments
 (0)