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
75 changes: 65 additions & 10 deletions .github/workflows/cron-conda.yml
Original file line number Diff line number Diff line change
Expand Up @@ -406,6 +406,12 @@ jobs:
needs.conda-update.outputs.CONDA_CHANGED == 'true'
permissions:
contents: write
# #2060: the Windows leg builds and smoke-tests only, and never blocks. Each
# step that can fail on it has an `id`, and a `continue-on-error` true on
# `windows-latest` alone, so a failure leaves the leg at success, and
# `Windows summary` reports it as a warning, as the KTLO legs do (#1672).
# No job-level `continue-on-error`, which would still turn the leg's check
# red (#1672), so a lost runner fails the job.
strategy:
fail-fast: false
matrix:
Expand All @@ -415,34 +421,50 @@ jobs:
- macos-latest
- macos-26-intel
- ubuntu-latest
# No Windows leg since 2023, and no Windows steps (#1967): adding the leg
# back means writing its steps, as test_conda_no_windows_steps_1967 pins.
# #2060: builds and smoke-tests win-64, and publishes nothing until
# its runs pass. Every upload step below is gated off it, so the
# `ANACONDA_TOKEN` this job's `environment:` holds reaches no step on it.
- windows-latest
# #1633: the supported tier, from .github/python-versions.toml through
# `conda-update` (#1660); create-release.yml's ``conda`` job says why
# there is no KTLO 3.10 leg.
python-version: ${{ fromJSON(needs.conda-update.outputs.PYTHON_CONDA) }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# No persisted token: nothing here runs `git` with credentials, and the
# Windows leg builds a package nobody has published yet (#2060).
- id: checkout
continue-on-error: ${{ matrix.os == 'windows-latest' }}
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: conda-${{ needs.conda-update.outputs.PCAPKIT_VERSION }}+${{ needs.conda-update.outputs.PCAPKIT_BUILD }}
fetch-depth: 0
persist-credentials: false

- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
- id: setup-python
continue-on-error: ${{ matrix.os == 'windows-latest' }}
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: ${{ matrix.python-version }}

# #1854: pcapkit is not installed here; see create-release.yml's step of
# the same name.
# the same name. `shell: bash` here and below, as Windows runs `pwsh`.
- name: Install and Setup
id: install
continue-on-error: ${{ matrix.os == 'windows-latest' }}
shell: bash
run: |
set -x

python -m pip install -U pip setuptools wheel

# #1827: pip evaluates conda/requirements.txt's platform markers here,
# on this leg's own runner, so Intel macOS fetches its own cryptography
# pin; the recipe's build.sh installs whatever this downloaded.
# pin; the recipe's build.sh, or bld.bat on Windows, installs whatever
# this downloaded.
- name: Setup Conda Distribution
id: download
continue-on-error: ${{ matrix.os == 'windows-latest' }}
shell: bash
run: |
set -x

Expand All @@ -451,6 +473,8 @@ jobs:

# See create-release.yml's step of the same name for the 3.15 exception.
- name: Conda environment creation and activation
id: miniconda
continue-on-error: ${{ matrix.os == 'windows-latest' }}
uses: conda-incubator/setup-miniconda@be893c923ea9cf1cf7cd510fbdde27c7e18cbdcb # v4.1.0
env:
PCAPKIT_VERSION: ${{ needs.conda-update.outputs.PCAPKIT_VERSION }}
Expand All @@ -467,13 +491,19 @@ jobs:
conda-remove-defaults: false
show-channel-urls: true

# A login shell, as for the build below: on Windows only that one has the
# `conda` setup-miniconda's `conda init` defines.
- name: Install conda build tools
id: conda-build-tools
continue-on-error: ${{ matrix.os == 'windows-latest' }}
shell: bash -el {0}
run: |
conda install -n base --override-channels -c conda-forge -y conda-build
conda build --version

- name: Build conda packages
id: conda_build
continue-on-error: ${{ matrix.os == 'windows-latest' }}
shell: bash -el {0}
env:
PCAPKIT_VERSION: ${{ needs.conda-update.outputs.PCAPKIT_VERSION }}
Expand Down Expand Up @@ -502,6 +532,9 @@ jobs:
ubuntu-latest)
target_platform="linux-64"
;;
windows-latest)
target_platform="win-64"
;;
*)
target_platform=""
;;
Expand Down Expand Up @@ -540,17 +573,27 @@ jobs:
# site-packages on the path. #1822: it also fails unless the package and
# every native binary in it are for this runner's platform.
# setup-python's interpreter, not conda's: it is the matrix version the
# package was built for, which conda's base is not on 3.15.
# package was built for, which conda's base is not on 3.15. On Windows it
# is `python.exe` at the top of `pythonLocation`, not under `bin/` (#2060).
- name: Smoke-test the vendored packages
id: smoke
if: steps.conda_build.outcome == 'success'
continue-on-error: ${{ matrix.os == 'windows-latest' }}
shell: bash
env:
PCAPKIT_CONDA_PACKAGE: ${{ steps.conda_build.outputs.host_package }}
run: |
"${pythonLocation}/bin/python" util/conda_vendor.py --smoke "$PCAPKIT_CONDA_PACKAGE"
python="${pythonLocation}/bin/python"
if [ "$RUNNER_OS" = Windows ]; then
python="$(cygpath -u "$pythonLocation")/python.exe"
fi
"$python" util/conda_vendor.py --smoke "$PCAPKIT_CONDA_PACKAGE"

# #2044: not on macos-26-intel, whose leg "Upload conda packages to
# Anaconda (Intel)" uploads instead.
# Anaconda (Intel)" uploads instead. #2060: nor on windows-latest, which
# publishes nothing until a run has shown its package works.
- name: Upload conda packages to Anaconda
if: ${{ matrix.os != 'macos-26-intel' }}
if: ${{ matrix.os != 'macos-26-intel' && matrix.os != 'windows-latest' }}
uses: anaconda/actions/upload-package@a6f488910b4a1e4f3fe01736241ca1b92b4b355a # v0.3.1
with:
token: ${{ secrets.ANACONDA_TOKEN }}
Expand Down Expand Up @@ -663,6 +706,18 @@ jobs:
tag: "v${{ needs.conda-update.outputs.PCAPKIT_VERSION }}"
token: "${{ secrets.GITHUB_TOKEN }}"

# #2060: the Windows leg's last step, as `KTLO summary` is a KTLO leg's
# (#1672): one warning per failed step, from each `outcome`. setup-python's
# `python`, or the image's where that failed.
- name: Windows summary
if: ${{ !cancelled() && matrix.os == 'windows-latest' }}
continue-on-error: true
shell: bash
env:
LEG: conda-dist on ${{ matrix.os }} with Python ${{ matrix.python-version }} (unpublished, non-blocking)
STEPS: ${{ toJSON(steps) }}
run: python util/leg_summary.py conda-windows

# #2030: posts `conda-update`'s report with the tag and upload results, and a
# short one of its own where `head` failed or was cancelled, as cron-vendor.yml's
# `discussion` job does, whose comment gives the rules. It waits for
Expand Down
19 changes: 15 additions & 4 deletions conda/pypcapkit/bld.bat
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
@ECHO OFF
SETLOCAL EnableDelayedExpansion

REM "%PYTHON%" -m pip install conda\wheels\* --target pcapkit/_extern -vv
FOR /F "Tokens=*" %A IN ('DIR /B conda\wheels\') DO @(
"%PYTHON%" -m pip install conda\wheels\%A --target pcapkit\_extern -vv
REM build.sh's two calls, for Windows (#2060). cmd does not expand the glob in
REM build.sh's `pip install conda/wheels/*`, so collect what Setup Conda
REM Distribution downloaded and install it in one call, letting pip resolve the
REM files together. In a batch file a FOR variable takes two percent signs; the
REM loop this replaces had one, a syntax error, and likely why no win-64 package
REM was ever built.
SET WHEELS=
FOR %%W IN (conda\wheels\*) DO SET WHEELS=!WHEELS! "%%W"
IF NOT DEFINED WHEELS (
ECHO bld.bat: conda\wheels holds nothing to install 1>&2
EXIT 1
)

"%PYTHON%" -m pip install . -vv
"%PYTHON%" -m pip install !WHEELS! --target pcapkit\_extern -vv
IF ERRORLEVEL 1 EXIT 1

"%PYTHON%" -m pip install . -vv
IF ERRORLEVEL 1 EXIT 1
9 changes: 9 additions & 0 deletions docs/source/contributing/workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -1548,6 +1548,15 @@ it, so the scheduled jobs reading them name it too
* Conda Update's ``conda-dist`` -- ``environment: anaconda``
* Vendor Update's ``vendor-update`` -- ``environment: conda-tag``

``conda-dist`` also has a ``windows-latest`` leg, which builds and smoke-tests a
``win-64`` package and publishes nothing
(`#2060 <https://github.com/JarryShaw/PyPCAPKit/issues/2060>`__). Every upload
step is gated off it, and those are the only steps that read ``ANACONDA_TOKEN``,
so no step on that leg receives the secret.
:file:`tests/project/test_conda_windows_leg_2060.py` checks both. Each step that
can fail there continues on error, and ``Windows summary`` reports each failure
as a warning, as ``KTLO summary`` does for a KTLO leg, so the leg stays green.

A required reviewer on ``conda-tag`` or ``anaconda`` would therefore hold every
scheduled bump for an approval, besides the release-graph reason
:doc:`releasing` gives for keeping them without one. Each run of these jobs
Expand Down
25 changes: 25 additions & 0 deletions tests/project/python_versions_fixtures/versions-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@
"arch": "x64",
"platform": "linux",
"platform_version": "26.04"
},
{
"filename": "python-3.15.0-win32-x64.zip",
"arch": "x64",
"platform": "win32"
}
]
},
Expand Down Expand Up @@ -64,6 +69,11 @@
"arch": "x64",
"platform": "linux",
"platform_version": "26.04"
},
{
"filename": "python-3.14.8-win32-x64.zip",
"arch": "x64",
"platform": "win32"
}
]
},
Expand Down Expand Up @@ -110,6 +120,11 @@
"arch": "x64",
"platform": "linux",
"platform_version": "26.04"
},
{
"filename": "python-3.13.16-win32-x64.zip",
"arch": "x64",
"platform": "win32"
}
]
},
Expand Down Expand Up @@ -168,6 +183,11 @@
"arch": "x64",
"platform": "linux",
"platform_version": "26.04"
},
{
"filename": "python-3.12.15-win32-x64.zip",
"arch": "x64",
"platform": "win32"
}
]
},
Expand Down Expand Up @@ -238,6 +258,11 @@
"arch": "x64",
"platform": "linux",
"platform_version": "26.04"
},
{
"filename": "python-3.11.17-win32-x64.zip",
"arch": "x64",
"platform": "win32"
}
]
},
Expand Down
4 changes: 2 additions & 2 deletions tests/project/test_conda_cryptography_pin_1827.py
Original file line number Diff line number Diff line change
Expand Up @@ -183,11 +183,11 @@ def test_a_dropped_or_moved_platform_pin_fails(self) -> None:

def test_an_override_no_build_can_use_fails(self) -> None:
lines = written()
pins = pinned(('cryptography', 'osx-64', '48.0.1'), ('cryptography', 'win-64', '1.0'),
pins = pinned(('cryptography', 'osx-64', '48.0.1'), ('cryptography', 'linux-aarch64', '1.0'),
('gone', 'osx-64', '1.0'))
with mock.patch.object(vendor(), 'PLATFORM_PINS', pins):
problems = self.problems('\n'.join(lines))
self.assertEqual(problems, ['override: cryptography is pinned for win-64, which no conda leg builds',
self.assertEqual(problems, ['override: cryptography is pinned for linux-aarch64, which no conda leg builds',
'override: gone is pinned for osx-64, and no conda build vendors it'])

def test_the_writer_refuses_an_override_no_build_can_use(self) -> None:
Expand Down
3 changes: 2 additions & 1 deletion tests/project/test_conda_intel_cli_upload_2044.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,8 +119,9 @@ def test_the_intel_leg_runs_the_cli_step_only(self) -> None:
self.assertTrue(runs(step(name, job_id, CLI_STEP), INTEL))

def test_every_other_leg_runs_the_action_only(self) -> None:
# #2060: but the Windows leg, which runs neither; test_conda_windows_leg_2060 pins that.
for name, job_id in JOBS:
others = [os for os in legs(name, job_id) if os != INTEL]
others = [os for os in legs(name, job_id) if os not in (INTEL, 'windows-latest')]
with self.subTest(workflow=name, job=job_id):
self.assertTrue(others, 'no other leg to check')
for os in others:
Expand Down
36 changes: 26 additions & 10 deletions tests/project/test_conda_native_platforms_1822.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@
* Both conda jobs map each runner label of their ``os`` axis to the conda
platform of that runner's own architecture, as GitHub's runner-images table
gives it (the snapshot in :file:`python_versions_fixtures`), and between them
publish exactly :data:`conda_vendor.PLATFORMS`.
build exactly :data:`conda_vendor.PLATFORMS`; win-64 only in Conda Update,
which does not publish it yet (#2060).
* Neither job runs ``conda convert``. Run as the job runs it, with a stand-in
``conda``, a leg publishes the one package conda built, and fails before any
upload when that is not the platform its label maps to.
* ``--smoke`` reads each native binary's ELF or Mach-O header and fails on one
* ``--smoke`` reads each native binary's ELF, Mach-O or PE header and fails on one
that is not for the package's platform, and on a package that is not for the
runner's own.

Expand Down Expand Up @@ -65,8 +66,11 @@
#: Intel, per https://docs.github.com/en/actions/reference/runners/github-hosted-runners
#: as read on 2026-10-16; the test below checks the snapshot of the image table too.
LEGS = {'macos-latest': 'osx-arm64', 'macos-26-intel': 'osx-64', 'ubuntu-latest': 'linux-64'}
#: Each job's legs: Conda Update's add #2060's unpublished ``windows-latest``.
JOB_LEGS = {('cron-conda.yml', 'conda-dist'): dict(LEGS, **{'windows-latest': 'win-64'}),
('create-release.yml', 'conda'): LEGS}
#: ``platform.machine()``, as the runner-images table names the architecture.
ARCH = {'x86_64': 'x64', 'arm64': 'arm64'}
ARCH = {'x86_64': 'x64', 'arm64': 'arm64', 'AMD64': 'x64'}

_MODULES = {} # type: dict[str, Any]

Expand Down Expand Up @@ -117,7 +121,7 @@ def test_each_leg_publishes_its_runners_own_platform(self) -> None:
self.assertTrue(blocks, f'{name} {job_id} maps no runner label')
for block in blocks:
with self.subTest(workflow=name, job=job_id):
self.assertEqual({label: block.get(label) for label in labels}, LEGS)
self.assertEqual({label: block.get(label) for label in labels}, JOB_LEGS[(name, job_id)])
for label in labels:
sys_platform, _, machine = vendor.PLATFORMS[block[label]]
with self.subTest(workflow=name, job=job_id, label=label):
Expand All @@ -127,8 +131,9 @@ def test_each_leg_publishes_its_runners_own_platform(self) -> None:

def test_the_legs_are_the_vendored_platforms(self) -> None:
"""What conda/requirements.txt is derived for is what the legs build."""
self.assertEqual(sorted(load('conda_vendor').PLATFORMS), sorted(LEGS.values()))
self.assertEqual(sorted(load('conda_vendor').BINARIES), sorted(LEGS.values()))
built = sorted({platform for legs in JOB_LEGS.values() for platform in legs.values()})
self.assertEqual(sorted(load('conda_vendor').PLATFORMS), built)
self.assertEqual(sorted(load('conda_vendor').BINARIES), built)


#: Stands in for ``conda``: ``build`` writes an empty package under
Expand Down Expand Up @@ -191,7 +196,7 @@ def build(self, name: 'str', job_id: 'str', label: 'str', built: 'str') -> 'tupl

def test_a_leg_publishes_only_the_package_conda_built(self) -> None:
for name, job_id in JOBS:
for label, platform in LEGS.items():
for label, platform in JOB_LEGS[(name, job_id)].items():
with self.subTest(workflow=name, label=label):
status, log, packages = self.build(name, job_id, label, platform)
self.assertEqual(status, 0, log)
Expand Down Expand Up @@ -219,6 +224,13 @@ def macho(cpu: 'int') -> 'bytes':
return b'\xcf\xfa\xed\xfe' + cpu.to_bytes(4, 'little') + bytes(4) + (8).to_bytes(4, 'little') + bytes(16)


def pe(machine: 'int', offset: 'int' = 0x80) -> 'bytes':
"""A PE image's header for COFF ``Machine``, its ``PE\\0\\0`` at ``offset`` (#2060)."""
head = bytearray(b'MZ\x90\x00' + bytes(offset - 4))
head[0x3c:0x40] = offset.to_bytes(4, 'little')
return bytes(head) + b'PE\0\0' + machine.to_bytes(2, 'little') + bytes(18)


def fat(*cpus: 'int') -> 'bytes':
"""A universal Mach-O's header with a slice per ``cputype``."""
return b'\xca\xfe\xba\xbe' + len(cpus).to_bytes(4, 'big') + b''.join(
Expand All @@ -235,12 +247,16 @@ def fat(*cpus: 'int') -> 'bytes':
'macho-arm64': (macho(ARM64), ('Mach-O', {'arm64'})),
'universal2': (fat(X86_64, ARM64), ('Mach-O', {'x86_64', 'arm64'})),
'fat-arm64': (fat(ARM64), ('Mach-O', {'arm64'})),
'pe-x86_64': (pe(0x8664), ('PE', {'x86_64'})),
'pe-arm64': (pe(0xaa64), ('PE', {'arm64'})),
'pe-i386': (pe(0x014c), ('PE', {'i386'})),
}
#: The stubs a package for each platform may carry.
ACCEPTS = {
'linux-64': {'elf-x86_64'},
'osx-64': {'macho-x86_64', 'universal2'},
'osx-arm64': {'macho-arm64', 'universal2', 'fat-arm64'},
'win-64': {'pe-x86_64'},
}


Expand Down Expand Up @@ -289,8 +305,8 @@ def test_other_files_are_passed_over_and_an_unreadable_library_is_not(self) -> N
(root / 'pkg' / 'libffi.so.8').write_text('not a library', encoding='utf-8')
count, problems = vendor.foreign(root, 'linux-64')
self.assertEqual(count, 0)
self.assertEqual(problems, ['pkg/_native.pyd: neither ELF nor Mach-O, so its machine is unknown',
'pkg/libffi.so.8: neither ELF nor Mach-O, so its machine is unknown'])
self.assertEqual(problems, ['pkg/_native.pyd: not ELF, Mach-O or PE, so its machine is unknown',
'pkg/libffi.so.8: not ELF, Mach-O or PE, so its machine is unknown'])

def test_noarch_carries_no_binary(self) -> None:
vendor = load('conda_vendor')
Expand All @@ -303,7 +319,7 @@ def test_the_runner_is_read_from_its_system_and_machine(self) -> None:
vendor = load('conda_vendor')
for here, subdir in ((('Linux', 'x86_64'), 'linux-64'), (('Darwin', 'x86_64'), 'osx-64'),
(('Darwin', 'arm64'), 'osx-arm64'), (('Linux', 'aarch64'), None),
(('Windows', 'AMD64'), None)):
(('Windows', 'AMD64'), 'win-64'), (('Windows', 'ARM64'), None)):
with self.subTest(here=here), mock.patch.object(vendor.platform, 'system', return_value=here[0]), \
mock.patch.object(vendor.platform, 'machine', return_value=here[1]):
self.assertEqual(vendor.runner_platform(), subdir)
Expand Down
Loading
Loading