diff --git a/install.ps1 b/install.ps1 index f1b5e2dc..3ecdc55f 100644 --- a/install.ps1 +++ b/install.ps1 @@ -8,7 +8,7 @@ param( $ErrorActionPreference = "Stop" $defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -$defaultInstallSha256 = "b163e90cc3101ce499e1f01899a398fbea27656054fb2fb9b31f07631d15f19a" +$defaultInstallSha256 = "133242b850a1e2dfa820f39b466c8b56a9363aee1cc8df1e9a1f9f28744a0cdc" $maximumInstallerBytes = 2MB $installerExit = 0 $installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null } diff --git a/install.py b/install.py index 7da9e794..8a4f3461 100644 --- a/install.py +++ b/install.py @@ -1134,6 +1134,24 @@ def keeps_case(directory: Path) -> bool: os.unlink(name) +def fold_case_twins(files: list[dict]) -> tuple[list[dict], list[str]]: + """One file per name where the directory folds case; the others named. + + The first in manifest order stays, as the pack's own order places it. + """ + kept: list[dict] = [] + seen: set[str] = set() + folded: list[str] = [] + for entry in files: + key = str(entry.get("dest", "")).casefold() + if key in seen: + folded.append(str(entry.get("dest", ""))) + continue + seen.add(key) + kept.append(entry) + return kept, folded + + def installed_cores(dirs: list[Path]) -> list[str]: """The cores present in these directories, by the name the buildbot uses. @@ -3218,10 +3236,22 @@ def _install(args: argparse.Namespace, real_stdout: TextIO) -> None: files = manifest.get("files", []) omitted_files = manifest.get("omitted_files", []) variants = manifest.get("case_variants", []) - if variants and keeps_case(bios_path): + case_kept = keeps_case(bios_path) + if variants and case_kept: # The list is folded for Windows and macOS; here bios.col and # BIOS.col are two files that two cores open. files = files + variants + if not case_kept: + files, folded = fold_case_twins(files) + if folded: + # Batocera's share opened over SMB, an exFAT card: the second + # spelling overwrote the first, and every --check found one + # of them wrong and fetched it again. + shown = ", ".join(folded[:3]) + (" ..." if len(folded) > 3 else "") + print( + f" {_plural(len(folded), 'file')} left out: {bios_path} does not keep " + f"case and holds another file under the same name ({shown})" + ) if args.list_targets: targets = fetch_targets(plat_name) diff --git a/install.sh b/install.sh index 8b8fac2f..810ce9a7 100755 --- a/install.sh +++ b/install.sh @@ -18,7 +18,7 @@ esac TEMP_INSTALLER="" TEMP_DIRECTORY="" DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py" -DEFAULT_INSTALL_SHA256="b163e90cc3101ce499e1f01899a398fbea27656054fb2fb9b31f07631d15f19a" +DEFAULT_INSTALL_SHA256="133242b850a1e2dfa820f39b466c8b56a9363aee1cc8df1e9a1f9f28744a0cdc" MAX_INSTALLER_BYTES=2097152 cleanup() { diff --git a/tests/test_install.py b/tests/test_install.py index 7abb9156..fe7f06e6 100644 --- a/tests/test_install.py +++ b/tests/test_install.py @@ -2245,7 +2245,7 @@ class CaseVariants(unittest.TestCase): compact = " ".join(source.split()) self.assertIn( 'variants = manifest.get("case_variants", []) ' - "if variants and keeps_case(bios_path):", compact, + "case_kept = keeps_case(bios_path) if variants and case_kept:", compact, ) def test_the_published_retroarch_list_carries_them(self): @@ -2318,5 +2318,22 @@ class TheBoardTheSystemNamesIsTheTarget(unittest.TestCase): self.assertTrue(targets) + +class CaseTwinsOnAFoldingVolume(unittest.TestCase): + """Batocera and Recalbox list np2kai/bios.rom and np2kai/BIOS.ROM, other + bytes each. Installed to a share opened from Windows or an exFAT card, + the second overwrote the first and --check never converged.""" + + def test_one_file_per_name_is_kept(self): + files = [{"dest": "np2kai/bios.rom"}, {"dest": "np2kai/BIOS.ROM"}, {"dest": "x.bin"}] + kept, folded = install.fold_case_twins(files) + self.assertEqual([f["dest"] for f in kept], ["np2kai/bios.rom", "x.bin"]) + self.assertEqual(folded, ["np2kai/BIOS.ROM"]) + + def test_main_folds_only_where_case_is_not_kept(self): + source = " ".join((REPO_ROOT / "install.py").read_text(encoding="utf-8").split()) + self.assertIn("if not case_kept: files, folded = fold_case_twins(files)", source) + + if __name__ == "__main__": unittest.main() diff --git a/wiki/troubleshooting.md b/wiki/troubleshooting.md index 4303a16d..1d5b51c5 100644 --- a/wiki/troubleshooting.md +++ b/wiki/troubleshooting.md @@ -95,6 +95,13 @@ BIOS directory keeps case and, if it does, also fetches these names. If you extract the ZIP on Linux by hand, take the Lakka pack instead: it is the RetroArch pack with both spellings. +The reverse happens on Batocera and Recalbox, whose lists hold both spellings +because their systems keep case: NP2kai reads `np2kai/bios.rom` and +`np2kai/BIOS.ROM` as two different files. Written to a folder that folds case +(the share opened from Windows, an exFAT card), one overwrites the other. The +installer checks the folder first, keeps one file per name there and lists +the ones it left out. + ## Hash mismatch / UNTESTED `verify.py` reports `UNTESTED` for a file.