Skip to content
Draft
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
33 changes: 33 additions & 0 deletions .ci/manifest.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Single source of truth for ecbuild's CI.
#
# ecbuild is a compiler-independent leaf producer: a CMake toolchain/module
# collection (project(ecbuild LANGUAGES C)) with no upstream package deps.
# fortmath, cxxmath and ecflowmath consume its install tree as a build tool, so
# they declare it in their [[deps]] with compiler-inputs = [] and fetch the
# ecbuild-<sha>-<os>-<build-type> artifact like any other dependency.
#
# Alongside this manifest:
# - ci.yml — push/PR build that publishes the artifact.
# - cross-repo-trigger.yml
# — workflow_dispatch entry point for consumer-driven
# recovery (from-job=rebuild-self), fired when a
# consumer finds the expected ecbuild artifact missing.

[package]
name = "ecbuild"
prefix = "ecbuild"
repo = "ecmwf-enterprise-sandbox/ecbuild"
# ecbuild produces a single compiler-independent install tree; no compiler is
# part of its artifact identity.
compiler-inputs = []

[[matrix.build.include]]
build-type = "Release"
os = "ubuntu-latest"

# No [[trigger-downstream]] entries: like stack-deps, consumers declare ecbuild
# in their own [[deps]] and recover a missing artifact by dispatching
# cross-repo-trigger.yml directly.
[matrix.build]
downstream-runnable = true
needs = []
35 changes: 35 additions & 0 deletions .github/actions/build-and-publish/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Build and publish ecbuild
description: >
Builds ecbuild and uploads its install tree as a workflow artifact under the
resolved canonical name. Shared by ci.yml (push/PR) and cross-repo-trigger.yml
(consumer-driven rebuild) so the build/publish path is defined exactly once.

inputs:
matrix-leg:
description: 'JSON-encoded full matrix leg (includes _resolved.* fields from the resolve job)'
required: true

runs:
using: composite
steps:
- name: Decode matrix-leg
id: m
shell: bash
run: |
leg='${{ inputs.matrix-leg }}'
echo "build-type=$(jq -r '."build-type"' <<<"$leg")" >> "$GITHUB_OUTPUT"
echo "own-artifact-name=$(jq -r '._resolved."own-artifact-name"' <<<"$leg")" >> "$GITHUB_OUTPUT"

- name: Build ecbuild
id: build
uses: ./.github/actions/build-ecbuild
with:
source-path: ${{ github.workspace }}
build-type: ${{ steps.m.outputs.build-type }}

- name: Publish ecbuild
uses: ecmwf-enterprise-sandbox/downstream-ci/actions/fetch-and-publish@main
with:
mode: publish
install-path: ${{ steps.build.outputs.install-path }}
artifact-name: ${{ steps.m.outputs.own-artifact-name }}
68 changes: 68 additions & 0 deletions .github/actions/build-ecbuild/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
name: Build ecbuild from source
description: >
Configures and installs ecbuild (a compiler-independent CMake toolchain
collection) to a canonical prefix derived from the shared install-prefix
action. When source-path is set the already checked-out tree is used; when
empty the given ref is checked out from the remote.

inputs:
ref:
description: 'Branch, tag, or SHA to check out (ignored when source-path is set)'
required: false
default: fix_interface_exports
source-path:
description: 'Path to an already checked-out source tree. Empty -> check out ref from remote.'
required: false
default: ''
build-type:
description: 'CMake build type'
required: false
default: Release
token:
description: 'GitHub token for checking out the source'
required: false
default: ''

outputs:
install-path:
description: 'Absolute path where ecbuild is installed'
value: ${{ steps.set-paths.outputs.install-path }}

runs:
using: composite
steps:
- name: Get install prefix
id: prefix
uses: ecmwf-enterprise-sandbox/downstream-ci/actions/install-prefix@main

- name: Set install path
id: set-paths
shell: bash
run: echo "install-path=${{ steps.prefix.outputs.base }}/ecbuild" >> "$GITHUB_OUTPUT"

- name: Checkout ecbuild source
if: inputs.source-path == ''
uses: actions/checkout@v6
with:
repository: ecmwf-enterprise-sandbox/ecbuild
ref: ${{ inputs.ref }}
path: _ecbuild-src
token: ${{ inputs.token || github.token }}

- name: Configure and install ecbuild
shell: bash
run: |
# Resolve the source tree from $GITHUB_WORKSPACE (env) rather than the
# github.workspace expression: the env var carries the in-container path
# on self-hosted container jobs, while the expression bakes the host path
# that does not exist inside the container. Works on GitHub-hosted too.
if [[ -n "${{ inputs.source-path }}" ]]; then
src="$GITHUB_WORKSPACE"
else
src="$GITHUB_WORKSPACE/_ecbuild-src"
fi
cmake -S "$src" \
-B "$RUNNER_TEMP/_ecbuild-build" \
-DCMAKE_INSTALL_PREFIX="${{ steps.set-paths.outputs.install-path }}" \
-DENABLE_TESTS=OFF
cmake --install "$RUNNER_TEMP/_ecbuild-build"
115 changes: 30 additions & 85 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,98 +1,43 @@
name: ci
name: CI

on:
# Trigger the workflow on push to master or develop, except tag creation
push:
branches:
- "master"
- "develop"
tags-ignore:
- "**"
paths-ignore:
- "docs/**"
- "bamboo/**"
- "README.rst"
- ".cd/**"
branches: [fix_interface_exports]
pull_request:

# Trigger the workflow on pull request
pull_request: ~

# Trigger the workflow manually
workflow_dispatch: ~

# Trigger after public PR approved for CI
pull_request_target:
types: [labeled]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

env:
ARTIFACT_POLL_INTERVAL: ${{ vars.ARTIFACT_POLL_INTERVAL || '60' }}

jobs:
# Run CI including downstream packages on self-hosted runners
downstream-ci:
name: downstream-ci
if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }}
uses: ecmwf/downstream-ci/.github/workflows/downstream-ci.yml@main
with:
ecbuild: ecmwf/ecbuild@${{ github.event.pull_request.head.sha || github.sha }}
codecov_upload: false
secrets: inherit

# Run CI of private downstream packages on self-hosted runners
private-downstream-ci:
name: private-downstream-ci
needs: [downstream-ci]
if: ${{ (success() || failure()) && (!github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci') }}
resolve:
runs-on: ubuntu-latest
permissions:
pull-requests: write
outputs:
build-matrix: ${{ steps.r.outputs.matrix-build }}
steps:
- name: Dispatch private downstream CI
uses: ecmwf/dispatch-private-downstream-ci@v1
- uses: actions/checkout@v6
- id: r
uses: ecmwf-enterprise-sandbox/downstream-ci/actions/resolve-deps@main
with:
token: ${{ secrets.GH_REPO_READ_TOKEN }}
owner: ecmwf
repository: private-downstream-ci
event_type: downstream-ci
payload: '{"ecbuild": "ecmwf/ecbuild@${{ github.event.pull_request.head.sha || github.sha }}"}'

# Build downstream packages on HPC
downstream-ci-hpc:
name: downstream-ci-hpc
if: ${{ !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }}
uses: ecmwf/downstream-ci/.github/workflows/downstream-ci-hpc.yml@main
with:
ecbuild: ecmwf/ecbuild@${{ github.event.pull_request.head.sha || github.sha }}
secrets: inherit

# Run CI of private downstream packages on HPC
private-downstream-ci-hpc:
name: private-downstream-ci-hpc
needs: [downstream-ci-hpc]
if: ${{ (success() || failure()) && (!github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci') }}
current-branch: ${{ github.head_ref || github.ref_name }}
matrix: build
token: ${{ secrets.ORG_READ_TOKEN || github.token }}
app-id: ${{ secrets.DOWNSTREAM_CI_APP_ID }}
app-private-key: ${{ secrets.DOWNSTREAM_CI_APP_PRIVATE_KEY }}

build:
needs: resolve
name: build (${{ matrix.build-type }})
runs-on: ubuntu-latest
permissions:
pull-requests: write
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.resolve.outputs.build-matrix) }}
steps:
- name: Dispatch private downstream CI
uses: ecmwf/dispatch-private-downstream-ci@v1
- uses: actions/checkout@v6
- name: Build and publish
uses: ./.github/actions/build-and-publish
with:
token: ${{ secrets.GH_REPO_READ_TOKEN }}
owner: ecmwf
repository: private-downstream-ci
event_type: downstream-ci-hpc
payload: '{"ecbuild": "ecmwf/ecbuild@${{ github.event.pull_request.head.sha || github.sha }}","skip_matrix_jobs": "nvidia-22.11"}'

# DISABLED FOR NOW UNTIL REPO IS SET UP FOR THIS
# notify:
# runs-on: ubuntu-latest
# needs:
# - downstream-ci
# - private-downstream-ci
# - downstream-ci-hpc
# - private-downstream-ci-hpc
# if: ${{ always() && !github.event.pull_request.head.repo.fork && github.event.action != 'labeled' || github.event.label.name == 'approved-for-ci' }}
# steps:
# - name: Trigger Teams notification
# uses: ecmwf/notify-teams@v1
# with:
# incoming_webhook: ${{ secrets.MS_TEAMS_INCOMING_WEBHOOK }}
# needs_context: ${{ toJSON(needs) }}
matrix-leg: ${{ toJSON(matrix) }}
92 changes: 92 additions & 0 deletions .github/workflows/cross-repo-trigger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
# Hand-written entry point for the consumer-driven recovery path. ecbuild is a
# leaf producer (no upstream package deps), so the generator can't infer it from
# the manifest alone; downstream-ci's write_or_check leaves this file in place.
# When a consumer (fortmath, cxxmath, ecflowmath) finds a missing ecbuild
# artifact, resolve_deps dispatches us here with from-job=rebuild-self.
name: Cross-repo trigger (ecbuild)

run-name: >-
Cross-repo trigger (${{ inputs.from-repo }}@${{ inputs.from-sha }})
[${{ inputs.dispatch-id }}]

on:
workflow_dispatch:
inputs:
dispatch-id:
description: 'Correlation id stamped into run-name so the dispatcher can find this run'
required: true
type: string
from-repo:
description: 'owner/repo of the dispatcher (downstream consumer)'
required: true
type: string
from-sha:
description: 'SHA of the dispatcher commit'
required: true
type: string
from-job:
description: >
For ecbuild the only accepted value is the sentinel 'rebuild-self';
there is no upstream package that could otherwise originate this dispatch.
required: true
type: string
branch:
description: 'Dispatcher branch name; we attempt branch-matching against it'
required: true
type: string
fallback-ref:
description: 'Ref to check out if branch does not exist in this repo'
required: false
type: string
default: 'fix_interface_exports'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

env:
ARTIFACT_POLL_INTERVAL: ${{ vars.ARTIFACT_POLL_INTERVAL || '60' }}

jobs:
resolve:
if: inputs.from-job == 'rebuild-self'
runs-on: ubuntu-latest
outputs:
ref: ${{ steps.pick.outputs.ref }}
matrix-build: ${{ steps.r.outputs.matrix-build }}
steps:
- id: pick
uses: ecmwf-enterprise-sandbox/downstream-ci/actions/pick-ref@main
with:
repo: ecmwf-enterprise-sandbox/ecbuild
try-branch: ${{ inputs.branch }}
fallback-ref: ${{ inputs.fallback-ref }}
token: ${{ secrets.ORG_READ_TOKEN || github.token }}
- uses: actions/checkout@v6
with:
ref: ${{ steps.pick.outputs.ref }}
- id: r
uses: ecmwf-enterprise-sandbox/downstream-ci/actions/resolve-deps@main
with:
current-branch: ${{ steps.pick.outputs.ref }}
matrix: build
token: ${{ secrets.ORG_READ_TOKEN || github.token }}
app-id: ${{ secrets.DOWNSTREAM_CI_APP_ID }}
app-private-key: ${{ secrets.DOWNSTREAM_CI_APP_PRIVATE_KEY }}

build:
needs: [resolve]
if: inputs.from-job == 'rebuild-self'
name: build (${{ matrix.build-type }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix: ${{ fromJSON(needs.resolve.outputs.matrix-build) }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.resolve.outputs.ref }}
- name: Build and publish
uses: ./.github/actions/build-and-publish
with:
matrix-leg: ${{ toJSON(matrix) }}
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#
# -DCMAKE_INSTALL_PREFIX=/path/to/install


cmake_minimum_required( VERSION 3.18 FATAL_ERROR )

set( CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH} )
Expand Down
4 changes: 2 additions & 2 deletions cmake/ecbuild_target_fortran_module_directory.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# granted to it by virtue of its status as an intergovernmental organisation
# nor does it submit to any jurisdiction.

macro( ecbuild_target_fortran_module_directory )
function( ecbuild_target_fortran_module_directory )
set( options NO_MODULE_DIRECTORY )
set( single_value_args TARGET MODULE_DIRECTORY INSTALL_MODULE_DIRECTORY )
set( multi_value_args "" )
Expand Down Expand Up @@ -35,4 +35,4 @@ macro( ecbuild_target_fortran_module_directory )
endif()
endif()

endmacro()
endfunction()
7 changes: 6 additions & 1 deletion cmake/project-config.cmake.in
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ if(EXISTS ${@PROJECT_NAME@_CMAKE_DIR}/@CONF_IMPORT_FILE@)
endif()

### insert definitions for IMPORTED targets
if(NOT @PROJECT_NAME@_BINARY_DIR)
# Guard: for install-tree exports (@PROJECT_NAME@_IS_BUILD_DIR_EXPORT=OFF) we must
# always load the targets file. For build-tree exports the targets are already
# defined by the build system, so we only skip when we are genuinely inside that
# build (i.e. the variable was set by ecbuild's own project() hook, not by an
# unrelated project that happens to share the same name).
if(NOT (@PROJECT_NAME@_IS_BUILD_DIR_EXPORT AND @PROJECT_NAME@_BINARY_DIR))
Comment on lines +45 to +49

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new guard/comment says we only skip loading targets when we are “genuinely inside” the exporting build (i.e. not an unrelated project with the same name). However the condition only checks @PROJECT_NAME@_IS_BUILD_DIR_EXPORT and that @PROJECT_NAME@_BINARY_DIR is non-empty; any unrelated project named @PROJECT_NAME@ will also define @PROJECT_NAME@_BINARY_DIR, so for build-tree exports this would still skip loading the targets file. Consider either (a) tightening the condition to verify @PROJECT_NAME@_BINARY_DIR actually matches this export’s build tree (e.g., compare against @PROJECT_NAME@_BASE_DIR / PACKAGE_PREFIX_DIR), or (b) adjusting the comment to match the actual behavior.

Suggested change
# always load the targets file. For build-tree exports the targets are already
# defined by the build system, so we only skip when we are genuinely inside that
# build (i.e. the variable was set by ecbuild's own project() hook, not by an
# unrelated project that happens to share the same name).
if(NOT (@PROJECT_NAME@_IS_BUILD_DIR_EXPORT AND @PROJECT_NAME@_BINARY_DIR))
# always load the targets file. For build-tree exports the targets are already
# defined by the build system, so we only skip when we are genuinely inside that
# build tree, i.e. when ecbuild's own project() hook has set
# @PROJECT_NAME@_BINARY_DIR to the same location as this export's base directory.
set(_@PROJECT_NAME@_INSIDE_EXPORTING_BUILD FALSE)
if(@PROJECT_NAME@_IS_BUILD_DIR_EXPORT AND DEFINED @PROJECT_NAME@_BINARY_DIR AND NOT "@PROJECT_NAME@_BINARY_DIR" STREQUAL "")
get_filename_component(_@PROJECT_NAME@_EXPORT_BASE_DIR "${@PROJECT_NAME@_BASE_DIR}" REALPATH)
get_filename_component(_@PROJECT_NAME@_CURRENT_BINARY_DIR "${@PROJECT_NAME@_BINARY_DIR}" REALPATH)
if(_@PROJECT_NAME@_EXPORT_BASE_DIR STREQUAL _@PROJECT_NAME@_CURRENT_BINARY_DIR)
set(_@PROJECT_NAME@_INSIDE_EXPORTING_BUILD TRUE)
endif()
endif()
if(NOT _@PROJECT_NAME@_INSIDE_EXPORTING_BUILD)

Copilot uses AI. Check for mistakes.
find_file(@PROJECT_NAME@_TARGETS_FILE
NAMES @PROJECT_NAME@-targets.cmake
HINTS @PACKAGE_TARGETS_DIRS@
Expand Down
Loading