mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 17:09:57 -04:00
Refuse a game folder whose path leaves no room for the game's files.
Windows and the build tools stop at 260 characters for a file's whole path, and a built game needs up to 128 below its folder. A deeper folder made the build stop in the middle with a file "not found"; Begin Installation and the install job now refuse it first and name both lengths. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
This commit is contained in:
@@ -135,6 +135,12 @@ game/ the built game, unless another folder was chosen (its l
|
||||
A path the Windows ANSI code page cannot hold breaks the MSYS2 tools, so the toolchain then goes to
|
||||
`C:\NX2navi\toolchain` instead (`launcher/paths.py`).
|
||||
|
||||
Windows and the build tools stop at 260 characters for a file's whole path, and the files of a built game need up to
|
||||
128 below the game folder (the prompt folders' resource table is the longest). Begin Installation and the install
|
||||
job therefore refuse a game folder whose own path is longer than 130 characters, with both lengths
|
||||
(`paths.long_path_problem`). Longer paths are not supported: the launcher does not use Windows' long path form, and
|
||||
the compiler, the linker and `tar` would not take it.
|
||||
|
||||
## Installing
|
||||
|
||||
`jobs.InstallJob`, one row per step:
|
||||
|
||||
@@ -9,7 +9,7 @@ import threading
|
||||
import time
|
||||
import traceback
|
||||
|
||||
from . import build, feed, gamefiles, locks, net, toolchain, updater, versions, winsys
|
||||
from . import build, feed, gamefiles, locks, net, paths, toolchain, updater, versions, winsys
|
||||
|
||||
RECHECK = 3600 # seconds between update checks while the launcher stays open, hidden during play included
|
||||
|
||||
@@ -456,6 +456,9 @@ class InstallJob(Job):
|
||||
return build.install_args(self.ctx.layout, self.dump, self.game_dir)
|
||||
|
||||
def run(self):
|
||||
too_long = paths.long_path_problem(self.game_dir) # the window asks first (begin_install); the command line here
|
||||
if too_long:
|
||||
raise build.BuildError(too_long)
|
||||
self.ensure_tools('tools')
|
||||
self.make_game(self.args(),
|
||||
{s: s for s in ('check', 'exefs', 'romfs', 'lift', 'compile', 'shaders', 'prompts', 'scan')})
|
||||
|
||||
@@ -38,6 +38,29 @@ def ansi_safe(path):
|
||||
return False
|
||||
|
||||
|
||||
# Windows: a file's whole path may be 259 characters for the programs that do not ask for more, and the build's tools
|
||||
# (the MSYS2 compiler, linker and resource compiler, tar) are among them whatever the system's long path setting.
|
||||
# LONGEST_IN_GAME is the room the game folder's own files need below it: the longest path a built game folder held
|
||||
# was 106 characters (romfs-patches-playstation-label\System\Resource\ResourceSizeTable...rsizetable.zs), and a file
|
||||
# is written under a temporary name up to 22 characters longer before it gets its own.
|
||||
WINDOWS_PATH_MAX = 259
|
||||
LONGEST_IN_GAME = 128
|
||||
|
||||
|
||||
def long_path_problem(game_dir):
|
||||
"""'' when the files of a game built into game_dir fit Windows' path length, else the sentence that says how long
|
||||
the folder's path is and how long it may be. Always '' elsewhere. Without it a build into a deep folder stops
|
||||
somewhere in the middle with "file not found" for a file that is there."""
|
||||
if os.name != 'nt' or not game_dir:
|
||||
return ''
|
||||
n, most = len(os.path.abspath(str(game_dir))), WINDOWS_PATH_MAX - 1 - LONGEST_IN_GAME
|
||||
if n <= most:
|
||||
return ''
|
||||
return (f'the path of the game folder is {n} characters long, and the game\'s files need {LONGEST_IN_GAME} more: '
|
||||
f'Windows and the build tools stop at {WINDOWS_PATH_MAX + 1} for a file\'s whole path. Choose a folder '
|
||||
f'whose path has at most {most} characters, such as D:\\Games\\NX2navi')
|
||||
|
||||
|
||||
def fixed_drive(drive):
|
||||
"""True when drive (X:\\) is a fixed disk (GetDriveTypeW DRIVE_FIXED), not a network, removable or optical drive.
|
||||
True where that cannot be asked."""
|
||||
|
||||
@@ -39,6 +39,7 @@ from tkinter import filedialog
|
||||
|
||||
from .. import NAME, build, feed, gameini, gamefiles, jobs, locks, mods, saves, steam, versions, winsys
|
||||
from ..context import Context
|
||||
from ..paths import long_path_problem
|
||||
from . import art, picture, sound
|
||||
|
||||
W, H = 1000, 640
|
||||
@@ -966,6 +967,11 @@ class App:
|
||||
self.choose_dump()
|
||||
return
|
||||
gd = self.game_dir()
|
||||
too_long = long_path_problem(gd) # told here, not as a missing file halfway through the build
|
||||
if too_long:
|
||||
self.message = App.friendly(too_long)
|
||||
self.render()
|
||||
return
|
||||
drop_partial_image(gd) # before the free space is measured
|
||||
if free_gb(gd) < 34 and not (Path(gd) / 'romfs' / 'romfs.bin').is_file():
|
||||
self.message = f'{drive_of(gd)} has {free_gb(gd):.0f} GB free; the game needs about 34 GB.'
|
||||
|
||||
@@ -5130,6 +5130,38 @@ class BeginInstallTest(Tmp):
|
||||
self.assertIn('31 GB free', rows['Install Folder'].desc[0])
|
||||
self.assertIn('(31 GB free)', rows['Begin Installation'].desc[0])
|
||||
|
||||
def test_a_game_folder_whose_path_is_too_long_is_refused_first(self):
|
||||
"""Windows and the build tools stop at 260 characters for a file's whole path, and the game folder's own
|
||||
files need 128 below it: a deeper folder is refused at Begin Installation and by the install job with the
|
||||
lengths, where the build stopped in the middle with a file "not found". No limit elsewhere."""
|
||||
from launcher import paths
|
||||
most = paths.WINDOWS_PATH_MAX - 1 - paths.LONGEST_IN_GAME
|
||||
fits = 'C:\\' + 'g' * (most - 3)
|
||||
deep = fits + 'g'
|
||||
with mock.patch.object(paths.os, 'name', 'nt'), \
|
||||
mock.patch.object(paths.os.path, 'abspath', lambda p: p): # as Windows gives these paths back
|
||||
self.assertEqual(paths.long_path_problem(fits), '')
|
||||
self.assertEqual(paths.long_path_problem(''), '')
|
||||
said = paths.long_path_problem(deep)
|
||||
self.assertIn(f'is {most + 1} characters long', said)
|
||||
self.assertIn(f'at most {most} characters', said)
|
||||
with mock.patch.object(paths.os, 'name', 'posix'):
|
||||
self.assertEqual(paths.long_path_problem('/' + 'g' * 4000), '')
|
||||
if os.name != 'nt':
|
||||
return
|
||||
App, a = mock_app(self, deep)
|
||||
a.settings['dump'] = str(self.tmp / 'game.nup')
|
||||
a.game_dir.return_value = deep
|
||||
App.begin_install(a)
|
||||
a.run_job.assert_not_called()
|
||||
self.assertTrue(a.message.startswith('The path of the game folder is'))
|
||||
ctx = JobTest.make_ctx(self, 'ok')
|
||||
self.addCleanup(ctx._file.close)
|
||||
j = jobs.InstallJob(ctx, self.tmp / 'g.nup', deep).run_inline()
|
||||
self.assertEqual(j.result, 'failed')
|
||||
self.assertIn('characters long', j.error)
|
||||
self.assertEqual(j.step('tools').state, jobs.PENDING) # before the build tools are fetched
|
||||
|
||||
|
||||
class UpdateTextTest(Tmp):
|
||||
def test_the_updates_row_does_not_promise_a_few_minutes(self):
|
||||
|
||||
Reference in New Issue
Block a user