Skip to content

Commit e63e838

Browse files
authored
Merge pull request #44 from DavidCEllis/fix_dtrun_paths
Move the data folder to a more standard location on linux
2 parents 161e364 + 57627b0 commit e63e838

File tree

6 files changed

+108
-17
lines changed

6 files changed

+108
-17
lines changed

README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ version automatically and use that to build the environment.
6060
Environment data and the application itself will be stored in the following locations:
6161

6262
* Windows: `%LOCALAPPDATA%\ducktools\env`
63-
* Linux/Mac/Other: `~/.ducktools/env`
63+
* Linux/Mac/Other:
64+
* Data: `~/.local/share/ducktools/env`
65+
* Config: `~/.config/ducktools/env` (Not yet used)
6466

6567
## Usage ##
6668

src/ducktools/env/__main__.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,17 @@ def get_parser(prog, exit_on_error=True) -> FixedArgumentParser:
227227
help="Name of the environment to delete",
228228
)
229229

230+
# Temporary migrate argument
231+
if sys.platform != "win32":
232+
migrate = subparsers.add_parser(
233+
"migrate",
234+
help="migrate old ducktools-env folder"
235+
)
236+
237+
migrate_mode = migrate.add_mutually_exclusive_group()
238+
migrate_mode.add_argument("--overwrite", action="store_true")
239+
migrate_mode.add_argument("--delete", action="store_true")
240+
230241
return parser
231242

232243

@@ -431,6 +442,20 @@ def delete_env_command(manager, args):
431442
return 0
432443

433444

445+
def migrate_command(manager, args):
446+
from .platform_paths import PACKAGE_SUBFOLDER, migrate_old_env
447+
folder_base = os.path.join(manager.project_name, PACKAGE_SUBFOLDER)
448+
if args.overwrite:
449+
mode = "overwrite"
450+
elif args.delete:
451+
mode = "delete"
452+
else:
453+
mode = "error"
454+
455+
migrate_old_env(folder_base, mode=mode)
456+
return 0
457+
458+
434459
def main_command() -> int:
435460
executable_name = os.path.splitext(os.path.basename(sys.executable))[0]
436461

@@ -479,6 +504,8 @@ def main_command() -> int:
479504
return list_command(manager, args)
480505
case "delete_env":
481506
return delete_env_command(manager, args)
507+
case "migrate":
508+
return migrate_command(manager, args)
482509
case _:
483510
raise RuntimeError(f"Invalid Command {args.command!r}")
484511

@@ -491,7 +518,7 @@ def main() -> int:
491518
if sys.stderr:
492519
sys.stderr.write(errors)
493520
return 1
494-
return 0
521+
return result
495522

496523

497524
if __name__ == "__main__":

src/ducktools/env/_run.py

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@
3939

4040

4141
def run():
42+
if len(sys.argv) < 2:
43+
# Script path is required
44+
sys.stderr.write("usage: dtrun script_filename [script_args ...]\n")
45+
sys.stderr.write("dtrun: error: the following arguments are required: script_filename\n")
46+
return 1
47+
4248
# First argument is the path to this script
4349
_, app, *args = sys.argv
4450

@@ -52,12 +58,12 @@ def run():
5258

5359
try:
5460
if os.path.isfile(app):
55-
manager.run_script(
61+
returncode = manager.run_script(
5662
script_path=app,
5763
script_args=args,
5864
)
5965
else:
60-
manager.run_registered_script(
66+
returncode = manager.run_registered_script(
6167
script_name=app,
6268
script_args=args,
6369
)
@@ -67,4 +73,4 @@ def run():
6773
sys.stderr.write(msg)
6874
return 1
6975

70-
return 0
76+
return returncode

src/ducktools/env/manager.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -534,7 +534,7 @@ def run_registered_script(
534534
script_args: list[str],
535535
generate_lock: bool = False,
536536
lock_path: str | None = None,
537-
) -> None:
537+
) -> int:
538538
try:
539539
row = self.script_registry.retrieve_script(script_name=script_name)
540540
except ScriptNotFound as e:
@@ -547,7 +547,7 @@ def run_registered_script(
547547

548548
script_path = row.path
549549

550-
self.run_script(
550+
return self.run_script(
551551
script_path=script_path,
552552
script_args=script_args,
553553
generate_lock=generate_lock,

src/ducktools/env/platform_paths.py

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import os
2828
import os.path
2929

30+
from ._logger import log
3031

3132
class UnsupportedPlatformError(Exception):
3233
pass
@@ -69,7 +70,7 @@ class UnsupportedPlatformError(Exception):
6970
USER_FOLDER = os.path.expanduser("~")
7071

7172

72-
def get_platform_python(venv_folder):
73+
def get_platform_python(venv_folder: str):
7374
if sys.platform == "win32":
7475
if sys.stdout:
7576
return os.path.join(venv_folder, "Scripts", "python.exe")
@@ -79,31 +80,70 @@ def get_platform_python(venv_folder):
7980
return os.path.join(venv_folder, "bin", "python")
8081

8182

82-
def get_platform_folder(name):
83+
def get_platform_folder(name: str, config: bool = False) -> str:
8384
if sys.platform == "win32":
84-
return os.path.join(USER_FOLDER, name)
85+
platform_folder = os.path.join(USER_FOLDER, name)
86+
elif config:
87+
platform_folder = os.path.join(USER_FOLDER, ".config", name)
8588
else:
86-
return os.path.join(USER_FOLDER, f".{name}")
89+
platform_folder = os.path.join(USER_FOLDER, ".local", "share", name)
90+
91+
return platform_folder
92+
93+
94+
def migrate_old_env(name: str, mode="error"):
95+
if sys.platform != "win32":
96+
old_folder = os.path.join(USER_FOLDER, f".{name}")
97+
new_folder = os.path.join(USER_FOLDER, ".local", "share", name)
98+
if os.path.exists(old_folder):
99+
print(f"Migrating from {old_folder!r} to {new_folder!r}")
100+
import shutil
101+
if os.path.exists(new_folder):
102+
if mode == "delete":
103+
print(f"Removing old data folder as new folder detected.")
104+
shutil.rmtree(old_folder)
105+
elif mode == "overwrite":
106+
print(f"Overwriting new folder with old folder data")
107+
shutil.rmtree(new_folder)
108+
shutil.move(old_folder, new_folder)
109+
else:
110+
raise RuntimeError(
111+
"Error: Both old and new env folders exist.\n"
112+
"Use --delete to remove the old folder or --overwrite to replace the new folder"
113+
)
114+
else:
115+
os.makedirs(os.path.dirname(new_folder), exist_ok=True)
116+
shutil.move(old_folder, new_folder)
117+
print(f"Moved old data to new folder")
118+
119+
# Try to remove the old folder, will only succeed if empty
120+
try:
121+
os.rmdir(os.path.dirname(old_folder))
122+
except OSError:
123+
pass
87124

88125

89126
class ManagedPaths:
90127
project_name: str
91128
project_folder: str
129+
130+
# WIN32: %LOCALAPPDATA%\ducktools\env
131+
# OTHER: ~/.config/ducktools/env
92132
config_path: str
93133

134+
# WIN32: %LOCALAPPDATA%\ducktools\env
135+
# OTHER: ~/.local/share/ducktools/env
94136
manager_folder: str
95137
pip_zipapp: str
96138
uv_executable: str
97139
env_folder: str
140+
register_db: str
98141

99142
application_folder: str
100143
application_db: str
101-
102144
cache_folder: str
103145
cache_db: str
104146

105-
register_db: str
106-
107147
build_base: str
108148

109149
def __init__(self, project_name="ducktools"):
@@ -112,7 +152,18 @@ def __init__(self, project_name="ducktools"):
112152
folder_base = os.path.join(self.project_name, PACKAGE_SUBFOLDER)
113153

114154
self.project_folder = get_platform_folder(folder_base)
115-
self.config_path = os.path.join(self.project_folder, CONFIG_FILENAME)
155+
156+
if sys.platform != "win32":
157+
if os.path.exists(os.path.join(USER_FOLDER, f".{folder_base}")):
158+
log(
159+
"Old ducktools-env folder detected, "
160+
"use the migrate subcommand to copy data to the new path."
161+
)
162+
163+
self.config_path = os.path.join(
164+
get_platform_folder(folder_base, config=True),
165+
CONFIG_FILENAME
166+
)
116167

117168
self.manager_folder = os.path.join(self.project_folder, MANAGER_FOLDERNAME)
118169
self.pip_zipapp = os.path.join(self.manager_folder, "pip.pyz")

tests/test_platform_paths.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,11 @@ def test_get_platform_folder():
3939
if sys.platform == "win32":
4040
assert platform_folder == str(USER_PATH / "demo")
4141
else:
42-
assert platform_folder == str(USER_PATH / ".demo")
42+
assert platform_folder == str(USER_PATH / ".local/share/demo")
43+
44+
if sys.platform != "win32":
45+
config_folder = get_platform_folder("demo", config=True)
46+
assert config_folder == str(USER_PATH / ".config/demo")
4347

4448

4549
class TestManagedPaths:
@@ -52,9 +56,10 @@ def test_basic_paths(self):
5256
# they are not accidentally changed.
5357

5458
project_folder = Path(get_platform_folder(self.project_name)) / "env"
59+
config_folder = Path(get_platform_folder(self.project_name, config=True)) / "env"
5560

5661
assert self.paths.project_folder == str(project_folder)
57-
assert self.paths.config_path == str(project_folder / "config.json")
62+
assert self.paths.config_path == str(config_folder / "config.json")
5863
assert self.paths.manager_folder == str(project_folder / "lib")
5964
assert self.paths.pip_zipapp == str(project_folder / "lib" / "pip.pyz")
6065
assert self.paths.env_folder == str(project_folder / "lib" / "ducktools-env")

0 commit comments

Comments
 (0)