pinokiod 8.2.10 → 8.2.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,7 +16,8 @@ const {
16
16
  isExpectedPythonPinned,
17
17
  TLS_PACKAGE_SPECS,
18
18
  PIP_MIN_VERSION,
19
- PIP_INSTALL_COMMAND,
19
+ PIP_WHEEL,
20
+ PIP_SEED_SCRIPT,
20
21
  } = require('../kernel/bin/conda-pins')
21
22
 
22
23
  test.beforeEach(t => {
@@ -24,6 +25,12 @@ test.beforeEach(t => {
24
25
  t.mock.method(childProcess, 'execFile', (binary, args, options, done) => {
25
26
  if (args[0] !== '-I') return realExecFile(binary, args, options, done)
26
27
  const prefix = path.basename(binary) === 'python.exe' ? path.dirname(binary) : path.dirname(path.dirname(binary))
28
+ if (args.includes(PIP_SEED_SCRIPT)) {
29
+ const marker = path.join(prefix, 'test-pip-seed')
30
+ const operation = args.includes('--check') ? fs.readFile(marker, 'utf8') : fs.writeFile(marker, 'ready')
31
+ operation.then(() => done(null, ''), error => done(error, '', error.message))
32
+ return
33
+ }
27
34
  assert.deepEqual(args, ['-I', '-m', 'pip', '--version'])
28
35
  fs.readFile(path.join(prefix, 'test-pip-version'), 'utf8').then(
29
36
  version => done(null, `pip ${version.trim()} from ${prefix} (python 3.10)`), error => done(error, '')
@@ -73,6 +80,7 @@ async function writeHealthyCondaMeta(root, platform = 'win32', condaVersion = CO
73
80
  await writeCondaMeta(root, name, '2026.10.19')
74
81
  }
75
82
  await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), PIP_MIN_VERSION)
83
+ await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-seed'), 'ready')
76
84
  }
77
85
 
78
86
  async function writeFakeManagedConda(root, platform = 'win32') {
@@ -112,6 +120,7 @@ function createConda(kernel) {
112
120
  const conda = new Conda()
113
121
  conda.kernel = kernel
114
122
  conda.verifyInstaller = async () => {} // Installer lifecycle tests mock the download.
123
+ conda.cachePipWheel = async () => kernel.bin.path(PIP_WHEEL.filename)
115
124
  return conda
116
125
  }
117
126
 
@@ -278,7 +287,7 @@ test('Conda install requests the Python 3.10.20 pin on macOS/Linux', async () =>
278
287
  assert.equal(await pathExists(path.join(root, 'bin', 'miniconda')), true)
279
288
  const condaInstall = calls.find((call) => Array.isArray(call.message))
280
289
  assert.ok(condaInstall)
281
- assert.equal(condaInstall.message[2], PIP_INSTALL_COMMAND)
290
+ assert.deepEqual(condaInstall.message[2], { _: ['python', '-I', '-m', 'pip', '--isolated', 'install', '--no-index', PIP_WHEEL.filename] })
282
291
  assert.equal(
283
292
  condaInstall.message[1],
284
293
  'conda install -y --override-channels -c conda-forge "conda=26.5.3" "python=3.10.20" "conda-libmamba-solver>=25.4.0" "ca-certificates>=2026.10.19" "certifi>=2026.10.19"'
@@ -639,6 +648,7 @@ test('tampered Miniforge installers stop immediately and leave the runtime untou
639
648
  kernel.bin.mod = { caddy: { running: () => assert.fail('must verify before stopping services') } }
640
649
  const conda = new Conda()
641
650
  conda.kernel = kernel
651
+ conda.cachePipWheel = async () => kernel.bin.path(PIP_WHEEL.filename)
642
652
  await assert.rejects(conda.install({}, () => {}), /SHA-256 verification failed/)
643
653
  assert.equal(downloads, 1)
644
654
  assert.equal(await pathExists(path.join(root, 'bin/miniforge/Scripts/conda.exe')), true)
@@ -667,6 +677,9 @@ test('certificate refresh preserves named environments and existing configuratio
667
677
  const dir = path.join(root, platform)
668
678
  await writeHealthyCondaMeta(dir, platform)
669
679
  await writeFakeManagedConda(dir, platform)
680
+ for (const name of ['ca-certificates', 'certifi']) {
681
+ await fs.rm(path.join(dir, 'bin/miniforge/conda-meta', `${name}-2026.10.19.json`))
682
+ }
670
683
  await fs.writeFile(path.join(dir, 'bin/miniforge/test-pip-version'), '23.0.1')
671
684
  const marker = path.join(dir, 'bin/miniforge/envs/app/keep')
672
685
  await fs.mkdir(path.dirname(marker), { recursive: true })
@@ -690,16 +703,150 @@ test('certificate refresh preserves named environments and existing configuratio
690
703
  const python = platform === 'win32' ? WINDOWS_PYTHON_SSL_FIX_SPEC : PYTHON_INSTALL_SPEC
691
704
  assert.deepEqual(calls[0].message, [
692
705
  `conda install -y --override-channels -c conda-forge "${python}" "conda-libmamba-solver>=25.4.0" ${TLS_PACKAGE_SPECS.map(spec => `"${spec}"`).join(' ')}`,
693
- PIP_INSTALL_COMMAND,
694
706
  ])
695
707
  }
696
708
  })
697
709
 
710
+ test('a missing or old solver is repaired in place when TLS and pip are already current', async t => {
711
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-solver-repair-'))
712
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
713
+ for (const platform of ['darwin', 'win32', 'linux']) {
714
+ for (const version of [null, '24.9.0', '25.3.0']) {
715
+ const dir = path.join(root, platform, String(version))
716
+ await writeHealthyCondaMeta(dir, platform)
717
+ await writeFakeManagedConda(dir, platform)
718
+ await fs.rm(path.join(dir, 'bin/miniforge/conda-meta/conda-libmamba-solver-25.4.0.json'))
719
+ if (version) await writeCondaMeta(dir, 'conda-libmamba-solver', version)
720
+ const marker = path.join(dir, 'bin/miniforge/envs/app/keep')
721
+ await fs.mkdir(path.dirname(marker), { recursive: true })
722
+ await fs.writeFile(marker, 'existing app')
723
+ const kernel = createKernel(dir, platform)
724
+ let installs = 0
725
+ kernel.bin.download = async () => assert.fail('must not replace the runtime')
726
+ kernel.bin.exec = async ({ message }) => {
727
+ installs++
728
+ assert.match(message[0], /conda install -y --solver classic /)
729
+ assert.match(message[0], /"conda-libmamba-solver>=25\.4\.0"/)
730
+ if (version) await fs.rm(path.join(dir, `bin/miniforge/conda-meta/conda-libmamba-solver-${version}.json`))
731
+ await writeHealthyCondaMeta(dir, platform)
732
+ return {}
733
+ }
734
+ const conda = createConda(kernel)
735
+ assert.equal(await conda.check(), false)
736
+ await conda.install({}, () => {})
737
+ assert.equal(await conda.check(), true)
738
+ await conda.install({}, () => {})
739
+ assert.equal(installs, 1, 'a retry uses the repaired solver')
740
+ assert.equal(await fs.readFile(marker, 'utf8'), 'existing app')
741
+ }
742
+ }
743
+ })
744
+
745
+ test('in-place repair checks pip after Conda changes it and never downgrades newer pip', async t => {
746
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-refresh-'))
747
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
748
+ for (const platform of ['darwin', 'win32', 'linux']) {
749
+ for (const [before, after] of [['27.0', '27.0'], ['27.0', '23.0.1'], ['23.0.1', '23.0.1'], ['23.0.1', '27.0']]) {
750
+ const dir = path.join(root, platform, before, after)
751
+ await writeHealthyCondaMeta(dir, platform)
752
+ await writeFakeManagedConda(dir, platform)
753
+ // Force a repair independently of the starting pip version.
754
+ await fs.rm(path.join(dir, 'bin/miniforge/conda-meta/conda-libmamba-solver-25.4.0.json'))
755
+ const version = path.join(dir, 'bin/miniforge/test-pip-version')
756
+ await fs.writeFile(version, before)
757
+ const marker = path.join(dir, 'bin/miniforge/envs/app/keep')
758
+ await fs.mkdir(path.dirname(marker), { recursive: true })
759
+ await fs.writeFile(marker, 'existing app')
760
+ const kernel = createKernel(dir, platform)
761
+ let pipInstalls = 0
762
+ kernel.bin.download = async () => assert.fail('must not replace the runtime')
763
+ kernel.bin.exec = async ({ message }) => {
764
+ if (Array.isArray(message)) {
765
+ assert.equal(message.length, 1, 'Conda repair must not include an unconditional pip install')
766
+ await writeHealthyCondaMeta(dir, platform)
767
+ await fs.writeFile(version, after)
768
+ } else {
769
+ pipInstalls++
770
+ assert.deepEqual(message, { _: ['python', '-I', '-m', 'pip', '--isolated', 'install', '--no-index', PIP_WHEEL.filename] })
771
+ await fs.writeFile(version, PIP_MIN_VERSION)
772
+ }
773
+ return {}
774
+ }
775
+ await createConda(kernel).install({}, () => {})
776
+ assert.equal(pipInstalls, after === '27.0' ? 0 : 1)
777
+ assert.equal(await fs.readFile(version, 'utf8'), after === '27.0' ? '27.0' : PIP_MIN_VERSION)
778
+ assert.equal(await fs.readFile(marker, 'utf8'), 'existing app')
779
+ }
780
+ }
781
+ })
782
+
783
+ test('old or missing pip is repaired without a Conda transaction when solver and certificates are current', async t => {
784
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-only-'))
785
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
786
+ for (const platform of ['darwin', 'win32', 'linux']) {
787
+ for (const version of ['23.0.1', null]) {
788
+ const dir = path.join(root, platform, String(version))
789
+ await writeHealthyCondaMeta(dir, platform)
790
+ await writeFakeManagedConda(dir, platform)
791
+ const pip = path.join(dir, 'bin/miniforge/test-pip-version')
792
+ if (version) await fs.writeFile(pip, version)
793
+ else await fs.rm(pip)
794
+ const seed = path.join(dir, 'bin/miniforge/test-pip-seed')
795
+ await fs.rm(seed)
796
+ const marker = path.join(dir, 'bin/miniforge/envs/app/keep')
797
+ await fs.mkdir(path.dirname(marker), { recursive: true })
798
+ await fs.writeFile(marker, 'existing app')
799
+ const kernel = createKernel(dir, platform)
800
+ let installs = 0
801
+ kernel.bin.download = async () => assert.fail('must not replace the runtime')
802
+ kernel.bin.exec = async ({ message }) => {
803
+ installs++
804
+ assert.deepEqual(message, { _: ['python', '-I', '-m', 'pip', '--isolated', 'install', '--no-index', PIP_WHEEL.filename] })
805
+ await fs.writeFile(pip, PIP_MIN_VERSION)
806
+ return {}
807
+ }
808
+ const conda = createConda(kernel)
809
+ conda._install = async () => assert.fail('must not reinstall the runtime')
810
+ await conda.install({}, () => {})
811
+ assert.equal(installs, 1, 'only the verified pip wheel is installed')
812
+ assert.equal(await conda.check(), true)
813
+ assert.equal(await fs.readFile(seed, 'utf8'), 'ready')
814
+ assert.equal(await fs.readFile(marker, 'utf8'), 'existing app')
815
+ }
816
+ }
817
+ })
818
+
819
+ test('failed in-place pip repair preserves app environments and does not proceed to seed repair', async t => {
820
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-refresh-fail-'))
821
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
822
+ await writeHealthyCondaMeta(root, 'darwin')
823
+ await writeFakeManagedConda(root, 'darwin')
824
+ const version = path.join(root, 'bin/miniforge/test-pip-version')
825
+ await fs.writeFile(version, '23.0.1')
826
+ const marker = path.join(root, 'bin/miniforge/envs/app/keep')
827
+ await fs.mkdir(path.dirname(marker), { recursive: true })
828
+ await fs.writeFile(marker, 'existing app')
829
+ const kernel = createKernel(root, 'darwin')
830
+ let calls = 0
831
+ kernel.bin.exec = async ({ message }) => {
832
+ calls++
833
+ assert.equal(Array.isArray(message), false, 'pip-only failure must not run Conda')
834
+ return { error: ['pip failed'] }
835
+ }
836
+ const conda = createConda(kernel)
837
+ conda.preparePipSeed = async () => assert.fail('a failed pip repair must stop setup')
838
+ conda._install = async () => assert.fail('must not replace the runtime')
839
+ await assert.rejects(conda.install({}, () => {}), /existing environments were preserved/)
840
+ assert.equal(calls, 1)
841
+ assert.equal(await fs.readFile(marker, 'utf8'), 'existing app')
842
+ })
843
+
698
844
  test('failed refresh reports an error without falling back to runtime replacement', async t => {
699
845
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-ca-failed-'))
700
846
  t.after(() => fs.rm(root, { recursive: true, force: true }))
701
847
  await writeHealthyCondaMeta(root, 'darwin')
702
848
  await writeFakeManagedConda(root, 'darwin')
849
+ await fs.rm(path.join(root, 'bin/miniforge/conda-meta/certifi-2026.10.19.json'))
703
850
  const marker = path.join(root, 'bin/miniforge/envs/app/keep')
704
851
  await fs.mkdir(path.dirname(marker), { recursive: true })
705
852
  await fs.writeFile(marker, 'preserve')
@@ -730,7 +877,7 @@ test('fresh installation retains readiness-based retries after package or pip er
730
877
  return {}
731
878
  }
732
879
  assert.deepEqual(payload.env, { PIP_REQUIRE_VIRTUALENV: 'false' })
733
- assert.equal(payload.message[2], PIP_INSTALL_COMMAND)
880
+ assert.deepEqual(payload.message[2], { _: ['python', '-I', '-m', 'pip', '--isolated', 'install', '--no-index', PIP_WHEEL.filename] })
734
881
  if (attempts === 1) return { error: ['ERROR: pip download failed'] }
735
882
  await fs.writeFile(path.join(root, 'bin/miniforge/test-pip-version'), PIP_MIN_VERSION)
736
883
  return {}
@@ -741,6 +888,47 @@ test('fresh installation retains readiness-based retries after package or pip er
741
888
  assert.equal(await conda.check(), true)
742
889
  })
743
890
 
891
+ test('post-install seed verification failure preserves the runtime and retries repair in place', async t => {
892
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-post-install-seed-'))
893
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
894
+ await fs.mkdir(path.join(root, 'bin'), { recursive: true })
895
+ const kernel = createKernel(root, 'darwin')
896
+ let downloads = 0
897
+ kernel.bin.mods = []
898
+ kernel.bin.download = async () => { downloads++ }
899
+ kernel.bin.rm = async () => {}
900
+ kernel.bin.exec = async payload => {
901
+ if (payload.conda?.skip) {
902
+ await writeHealthyCondaMeta(root, 'darwin')
903
+ await writeFakeManagedConda(root, 'darwin')
904
+ }
905
+ return {}
906
+ }
907
+ const conda = createConda(kernel)
908
+ const prepare = conda.preparePipSeed.bind(conda)
909
+ const marker = path.join(root, 'bin/miniforge/envs/app/keep')
910
+ let damaged = false
911
+ conda.preparePipSeed = async ondata => {
912
+ await prepare(ondata)
913
+ if (!damaged) {
914
+ damaged = true
915
+ await fs.mkdir(path.dirname(marker), { recursive: true })
916
+ await fs.writeFile(marker, 'preserve')
917
+ // Lose the seed after preparation, before the final readiness check.
918
+ await fs.rm(path.join(root, 'bin/miniforge/test-pip-seed'))
919
+ }
920
+ }
921
+ await assert.rejects(conda.install({}, () => {}), /pip seed verification failed; the runtime was preserved/)
922
+ assert.equal(downloads, 1)
923
+ assert.equal(await fs.readFile(marker, 'utf8'), 'preserve')
924
+ assert.equal(await conda.check(), false)
925
+ kernel.bin.exec = async () => assert.fail('retry must repair the seed without reinstalling the runtime')
926
+ await conda.install({}, () => {})
927
+ assert.equal(downloads, 1)
928
+ assert.equal(await fs.readFile(marker, 'utf8'), 'preserve')
929
+ assert.equal(await conda.check(), true)
930
+ })
931
+
744
932
  test('pip readiness accepts two-part release versions through the actual pip CLI', async t => {
745
933
  const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-release-'))
746
934
  t.after(() => fs.rm(root, { recursive: true, force: true }))
@@ -754,3 +942,65 @@ test('pip readiness accepts two-part release versions through the actual pip CLI
754
942
  assert.equal(bin.correct_conda, true)
755
943
  }
756
944
  })
945
+
946
+ test('pip wheel cache verifies downloads, reuses valid data and cleans failed attempts', async t => {
947
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-pip-cache-'))
948
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
949
+ await fs.mkdir(path.join(root, 'bin'))
950
+ const kernel = createKernel(root, 'darwin')
951
+ const conda = new Conda()
952
+ conda.kernel = kernel
953
+ const bytes = Buffer.from('test wheel')
954
+ const originalHash = PIP_WHEEL.sha256
955
+ PIP_WHEEL.sha256 = require('node:crypto').createHash('sha256').update(bytes).digest('hex')
956
+ t.after(() => { PIP_WHEEL.sha256 = originalHash })
957
+ const destination = kernel.bin.path(PIP_WHEEL.filename)
958
+ let downloads = 0
959
+ kernel.bin.download = async (url, name) => {
960
+ assert.equal(url, PIP_WHEEL.url)
961
+ downloads++
962
+ await fs.writeFile(kernel.bin.path(name), downloads === 1 ? 'bad download' : bytes)
963
+ }
964
+ await assert.rejects(conda.cachePipWheel(() => {}), /SHA-256 verification failed/)
965
+ assert.deepEqual(await fs.readdir(kernel.bin.path()), [])
966
+ assert.equal(await conda.cachePipWheel(() => {}), destination)
967
+ assert.equal(await conda.cachePipWheel(() => {}), destination)
968
+ assert.equal(downloads, 2)
969
+ await fs.writeFile(destination, 'damaged cache')
970
+ await Promise.all([conda.cachePipWheel(() => {}), conda.cachePipWheel(() => {})])
971
+ assert.deepEqual(await fs.readFile(destination), bytes)
972
+ assert.deepEqual(await fs.readdir(kernel.bin.path()), [PIP_WHEEL.filename])
973
+ })
974
+
975
+ test('seed-only damage and repair failure never reinstall Conda or delete app environments', async t => {
976
+ const root = await fs.mkdtemp(path.join(os.tmpdir(), 'pinokio-seed-only-'))
977
+ t.after(() => fs.rm(root, { recursive: true, force: true }))
978
+ for (const platform of ['darwin', 'win32', 'linux']) {
979
+ const dir = path.join(root, platform)
980
+ await writeHealthyCondaMeta(dir, platform)
981
+ await writeFakeManagedConda(dir, platform)
982
+ await fs.rm(path.join(dir, 'bin/miniforge/test-pip-seed'))
983
+ const marker = path.join(dir, 'bin/miniforge/envs/app/keep')
984
+ await fs.mkdir(path.dirname(marker), { recursive: true })
985
+ await fs.writeFile(marker, 'preserve')
986
+ const kernel = createKernel(dir, platform)
987
+ kernel.bin.exec = async () => assert.fail('seed repair must not run Conda install')
988
+ kernel.bin.download = async () => assert.fail('seed repair must not download a runtime installer')
989
+ const conda = createConda(kernel)
990
+ const repair = conda.preparePipSeed.bind(conda)
991
+ assert.equal(await conda.check(), false)
992
+ const bin = createBin(dir, platform)
993
+ await bin.tryList()
994
+ assert.equal(bin.correct_conda, false)
995
+ conda.preparePipSeed = async () => { throw new Error('simulated seed repair failure') }
996
+ await assert.rejects(conda.install({}, () => {}), /simulated seed repair failure/)
997
+ assert.equal(await fs.readFile(marker, 'utf8'), 'preserve')
998
+ conda.preparePipSeed = repair
999
+ await conda.install({}, () => {})
1000
+ assert.equal(await conda.check(), true)
1001
+ assert.equal(await pathExists(path.join(dir, 'bin/miniconda')), true)
1002
+ await bin.tryList()
1003
+ assert.equal(bin.correct_conda, true)
1004
+ assert.equal(await fs.readFile(marker, 'utf8'), 'preserve')
1005
+ }
1006
+ })
@@ -0,0 +1,183 @@
1
+ import hashlib
2
+ import importlib.util
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import py_compile
7
+ import stat
8
+ import sys
9
+ import tempfile
10
+ import types
11
+ import unittest
12
+ from unittest.mock import patch
13
+ import zipfile
14
+
15
+
16
+ SCRIPT = Path(__file__).resolve().parents[1] / "system" / "repair_pip_seed.py"
17
+ spec = importlib.util.spec_from_file_location("repair_pip_seed", SCRIPT)
18
+ seed = importlib.util.module_from_spec(spec)
19
+ spec.loader.exec_module(seed)
20
+
21
+
22
+ class PipSeedTests(unittest.TestCase):
23
+ def setUp(self):
24
+ self.temporary = tempfile.TemporaryDirectory(prefix="pip seed O'Brien & (test) ")
25
+ self.addCleanup(self.temporary.cleanup)
26
+ self.prefix = Path(self.temporary.name).resolve()
27
+ self.directory = self.prefix / "Lib" / "ensurepip"
28
+ self.bundled = self.directory / "_bundled"
29
+ self.bundled.mkdir(parents=True)
30
+ self.source = self.directory / "__init__.py"
31
+ self.original = b'_PIP_VERSION = "23.0.1"\n_SETUPTOOLS_VERSION = "79.0.1"\n'
32
+ self.source.write_bytes(self.original)
33
+ self.source.chmod(0o640 if os.name != "nt" else 0o666)
34
+ self.source_mode = stat.S_IMODE(self.source.stat().st_mode)
35
+ self.old_wheel = self.bundled / "pip-23.0.1-py3-none-any.whl"
36
+ self.old_wheel.write_bytes(b"old wheel stays")
37
+ self.setuptools = self.bundled / "setuptools-79.0.1.whl"
38
+ self.setuptools.write_bytes(b"keep setuptools")
39
+ self.legacy = self.prefix / "existing-venv" / "pip"
40
+ self.legacy.parent.mkdir()
41
+ self.legacy.write_bytes(b"existing pip stays")
42
+ self.pin = json.loads(SCRIPT.with_name("pip_seed.json").read_text())
43
+ self.wheel = self.prefix / self.pin["filename"]
44
+ self.wheel.write_bytes(b"verified fixture wheel")
45
+ self.pin["sha256"] = hashlib.sha256(self.wheel.read_bytes()).hexdigest()
46
+ self.destination = self.bundled / self.pin["filename"]
47
+ self.module = types.ModuleType("ensurepip")
48
+ self.module.__file__ = str(self.source)
49
+ self.module.version = lambda: self.source.read_text().split('"')[1]
50
+ for mock in [patch.object(sys, "prefix", str(self.prefix)),
51
+ patch.object(sys, "base_prefix", str(self.prefix)),
52
+ patch.dict(sys.modules, {"ensurepip": self.module}),
53
+ patch("json.loads", return_value=self.pin)]:
54
+ mock.start()
55
+ self.addCleanup(mock.stop)
56
+
57
+ def repair(self, check=False):
58
+ seed.repair(str(self.wheel), str(self.prefix), check)
59
+
60
+ def test_hardlinks_permissions_packages_and_repeat(self):
61
+ alias = self.prefix / "conda-cache-source"
62
+ os.link(self.source, alias)
63
+ wheel_alias = self.prefix / "conda-cache-wheel"
64
+ self.destination.write_bytes(b"corrupt old destination")
65
+ os.link(self.destination, wheel_alias)
66
+ self.repair()
67
+ self.assertEqual(alias.read_bytes(), self.original)
68
+ self.assertEqual(wheel_alias.read_bytes(), b"corrupt old destination")
69
+ self.assertEqual(stat.S_IMODE(self.source.stat().st_mode), self.source_mode)
70
+ self.assertEqual(self.old_wheel.read_bytes(), b"old wheel stays")
71
+ self.assertEqual(self.setuptools.read_bytes(), b"keep setuptools")
72
+ self.assertEqual(self.legacy.read_bytes(), b"existing pip stays")
73
+ inode = self.source.stat().st_ino
74
+ self.repair()
75
+ self.assertEqual(self.source.stat().st_ino, inode)
76
+ self.repair(check=True)
77
+
78
+ def test_bad_download_changes_nothing(self):
79
+ self.wheel.write_bytes(b"bad download")
80
+ with self.assertRaisesRegex(RuntimeError, "SHA-256"):
81
+ self.repair()
82
+ self.assertEqual(self.source.read_bytes(), self.original)
83
+ self.assertFalse(self.destination.exists())
84
+
85
+ def test_interrupted_source_publish_is_retryable(self):
86
+ replace = os.replace
87
+
88
+ def interrupt(source, destination):
89
+ if Path(destination) == self.source:
90
+ raise PermissionError("simulated source replacement failure")
91
+ replace(source, destination)
92
+
93
+ with patch("os.replace", side_effect=interrupt):
94
+ with self.assertRaises(PermissionError):
95
+ self.repair()
96
+ self.assertEqual(self.source.read_bytes(), self.original)
97
+ self.assertEqual(self.destination.read_bytes(), self.wheel.read_bytes())
98
+ self.assertEqual(self.old_wheel.read_bytes(), b"old wheel stays")
99
+ self.assertEqual(sorted(p.name for p in self.directory.iterdir()), ["__init__.py", "_bundled"])
100
+ self.repair()
101
+ self.repair(check=True)
102
+
103
+ def test_readiness_detects_missing_and_corrupt_seed_and_cache(self):
104
+ self.repair()
105
+ for value in [None, b"corrupt seed"]:
106
+ if value is None:
107
+ self.destination.unlink()
108
+ else:
109
+ self.destination.write_bytes(value)
110
+ with self.assertRaises((RuntimeError, FileNotFoundError)):
111
+ self.repair(check=True)
112
+ self.repair()
113
+ self.repair(check=True)
114
+ self.wheel.unlink()
115
+ with self.assertRaises(FileNotFoundError):
116
+ self.repair(check=True)
117
+
118
+ def test_same_size_source_update_invalidates_bytecode(self):
119
+ cache = Path(py_compile.compile(str(self.source), doraise=True))
120
+ before = self.source.stat()
121
+ self.repair()
122
+ self.assertEqual(self.source.stat().st_size, before.st_size)
123
+ self.assertFalse(cache.exists())
124
+ self.assertEqual(self.module.version(), self.pin["version"])
125
+ # A Conda Python reinstall restores the old seed reference.
126
+ self.source.write_bytes(self.original)
127
+ with self.assertRaisesRegex(RuntimeError, "needs repair"):
128
+ self.repair(check=True)
129
+ self.repair()
130
+ self.repair(check=True)
131
+
132
+ def test_newer_seed_is_preserved_or_repaired_when_incomplete(self):
133
+ newer_source = self.original.replace(b"23.0.1", b"27.0")
134
+ self.source.write_bytes(newer_source)
135
+ newer = self.bundled / "pip-27.0-py3-none-any.whl"
136
+ with zipfile.ZipFile(newer, "w") as archive:
137
+ archive.writestr("pip/__init__.py", "__version__ = '27.0'")
138
+ archive.writestr("pip-27.0.dist-info/METADATA", "Name: pip\nVersion: 27.0\n")
139
+ before = newer.read_bytes()
140
+ self.repair()
141
+ self.repair(check=True)
142
+ self.assertEqual(self.source.read_bytes(), newer_source)
143
+ self.assertEqual(newer.read_bytes(), before)
144
+ self.assertFalse(self.destination.exists())
145
+ newer.unlink()
146
+ with self.assertRaisesRegex(RuntimeError, "needs repair"):
147
+ self.repair(check=True)
148
+ self.repair()
149
+ self.repair(check=True)
150
+
151
+ def test_unsupported_python_and_wrong_prefix_cannot_be_modified(self):
152
+ with patch.object(sys, "version_info", (3, 9, 0)):
153
+ self.repair()
154
+ with self.assertRaisesRegex(RuntimeError, "3.10"):
155
+ self.repair(check=True)
156
+ self.assertEqual(self.source.read_bytes(), self.original)
157
+ with self.assertRaisesRegex(RuntimeError, "selected Python prefix"):
158
+ seed.repair(str(self.wheel), str(self.prefix / "another"))
159
+ with patch.object(sys, "base_prefix", str(self.prefix / "another")):
160
+ with self.assertRaisesRegex(RuntimeError, "selected Python prefix"):
161
+ self.repair()
162
+ self.assertFalse(self.destination.exists())
163
+
164
+ def test_unrecognized_layout_is_rejected_without_changes(self):
165
+ self.source.write_bytes(b"# unexpected vendor layout\n")
166
+ with self.assertRaisesRegex(RuntimeError, "Unrecognized"):
167
+ self.repair()
168
+ self.assertFalse(self.destination.exists())
169
+
170
+ @unittest.skipIf(os.name == "nt", "directory symlinks may require Windows administrator rights")
171
+ def test_bundled_directory_cannot_redirect_writes_outside_the_prefix(self):
172
+ import shutil
173
+ with tempfile.TemporaryDirectory() as other:
174
+ shutil.rmtree(self.bundled)
175
+ self.bundled.symlink_to(other, target_is_directory=True)
176
+ with self.assertRaisesRegex(RuntimeError, "selected Python prefix"):
177
+ self.repair()
178
+ self.assertEqual(list(Path(other).iterdir()), [])
179
+ self.assertEqual(self.source.read_bytes(), self.original)
180
+
181
+
182
+ if __name__ == "__main__":
183
+ unittest.main()