forked from python/pymanager
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfirstrun.py
More file actions
481 lines (432 loc) · 18.1 KB
/
firstrun.py
File metadata and controls
481 lines (432 loc) · 18.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
import os
import sys
import time
import winreg
from . import logging
from .pathutils import Path
LOGGER = logging.LOGGER
def _package_name():
from _native import get_current_package
return get_current_package()
def check_app_alias(cmd):
LOGGER.debug("Checking app execution aliases")
# Expected identities:
# Side-loaded MSIX
# * "PythonSoftwareFoundation.PythonManager_3847v3x7pw1km",
# Store package
# * "PythonSoftwareFoundation.PythonManager_qbz5n2kfra8p0",
# Development build
# * "PythonSoftwareFoundation.PythonManager_m8z88z54g2w36",
# MSI/dev install
# * None
try:
pkg = _package_name()
except OSError:
LOGGER.debug("Failed to get current package name.", exc_info=True)
pkg = None
if not pkg:
LOGGER.debug("Check skipped: MSI install can't do this check")
return "skip"
from _native import read_alias_package
LOGGER.debug("Checking for %s", pkg)
root = Path(os.environ["LocalAppData"]) / "Microsoft/WindowsApps"
for name in ["py.exe", "pyw.exe", "python.exe", "pythonw.exe", "python3.exe", "pymanager.exe"]:
exe = root / name
try:
LOGGER.debug("Reading from %s", exe)
package = read_alias_package(exe)
LOGGER.debug("Package: %s", package)
if package != pkg:
LOGGER.debug("Check failed: package did not match identity")
return False
except FileNotFoundError:
LOGGER.debug("Check failed: did not find %s", exe)
return False
LOGGER.debug("Check passed: aliases are correct")
return True
_LONG_PATH_KEY = r"System\CurrentControlSet\Control\FileSystem"
_LONG_PATH_VALUENAME = "LongPathsEnabled"
def check_long_paths(
cmd,
*,
hive=winreg.HKEY_LOCAL_MACHINE,
keyname=_LONG_PATH_KEY,
valuename=_LONG_PATH_VALUENAME,
):
LOGGER.debug("Checking long paths setting")
try:
with winreg.OpenKeyEx(hive, keyname) as key:
if winreg.QueryValueEx(key, valuename) == (1, winreg.REG_DWORD):
LOGGER.debug("Check passed: registry key is OK")
return True
except FileNotFoundError:
pass
LOGGER.debug("Check failed: registry key was missing or incorrect")
return False
def do_configure_long_paths(
cmd,
*,
hive=winreg.HKEY_LOCAL_MACHINE,
keyname=_LONG_PATH_KEY,
valuename=_LONG_PATH_VALUENAME,
startfile=os.startfile,
):
LOGGER.debug("Updating long paths setting")
try:
with winreg.CreateKeyEx(hive, keyname) as key:
winreg.SetValueEx(key, valuename, None, winreg.REG_DWORD, 1)
LOGGER.info("The setting has been successfully updated, and will "
"take effect after the next reboot.")
return
except OSError:
pass
if not cmd.confirm:
# Without confirmation, we assume we can't elevate, so attempt
# as the current user and if it fails just print a message.
LOGGER.warn("The setting has not been updated. Please rerun '!B!py "
"install --configure!W! with administrative privileges.")
return
startfile(sys.executable, "runas", "**configure-long-paths", show_cmd=0)
for _ in range(5):
time.sleep(0.25)
if check_long_paths(cmd):
LOGGER.info("The setting has been successfully updated, and will "
"take effect after the next reboot.")
break
else:
LOGGER.warn("The setting may not have been updated. Please "
"visit the additional help link at the end for "
"more assistance.")
def check_py_on_path(cmd):
LOGGER.debug("Checking for legacy py.exe on PATH")
from _native import read_alias_package
try:
if not _package_name():
LOGGER.debug("Check skipped: MSI install can't do this check")
return "skip"
except OSError:
LOGGER.debug("Failed to get current package name.", exc_info=True)
LOGGER.debug("Check skipped: can't do this check")
return "skip"
for p in os.environ["PATH"].split(";"):
if not p:
continue
py = Path(p) / "py.exe"
try:
read_alias_package(py)
LOGGER.debug("Check passed: found alias at %s", py)
# We found the alias, so we're good
return True
except FileNotFoundError:
pass
except OSError:
# Probably not an alias, so we're not good
LOGGER.debug("Check failed: found %s on PATH", py)
return False
LOGGER.debug("Check passed: no py.exe on PATH at all")
return True
def check_global_dir(cmd):
LOGGER.debug("Checking for global dir on PATH")
if not cmd.global_dir:
LOGGER.debug("Check skipped: global dir is not configured")
return "skip"
for p in os.environ["PATH"].split(";"):
if not p:
continue
if Path(p).absolute().match(cmd.global_dir):
LOGGER.debug("Check passed: %s is on PATH", p)
return True
# In case user has updated their registry but not the terminal
try:
r = _check_global_dir_registry(cmd)
if r:
return r
except Exception:
LOGGER.debug("Failed to read PATH setting from registry", exc_info=True)
LOGGER.debug("Check failed: %s not found in PATH", cmd.global_dir)
return False
def _check_global_dir_registry(cmd):
with winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, "Environment") as key:
path, kind = winreg.QueryValueEx(key, "Path")
LOGGER.debug("Current registry path: %s", path)
if kind == winreg.REG_EXPAND_SZ:
path = os.path.expandvars(path)
elif kind != winreg.REG_SZ:
LOGGER.debug("Check skipped: PATH registry key is not a string.")
return "skip"
for p in path.split(";"):
if not p:
continue
if Path(p).absolute().match(cmd.global_dir):
LOGGER.debug("Check skipped: %s will be on PATH after restart", p)
return True
return False
def do_global_dir_on_path(cmd):
added = notified = False
try:
LOGGER.debug("Adding %s to PATH", cmd.global_dir)
with winreg.OpenKeyEx(winreg.HKEY_CURRENT_USER, "Environment") as key:
initial, kind = winreg.QueryValueEx(key, "Path")
LOGGER.debug("Initial path: %s", initial)
if kind not in (winreg.REG_SZ, winreg.REG_EXPAND_SZ) or not isinstance(initial, str):
LOGGER.debug("Value kind is %s and not REG_[EXPAND_]SZ. Aborting.", kind)
return
for p in initial.split(";"):
if not p:
continue
if p.casefold() == str(cmd.global_dir).casefold():
LOGGER.debug("Path is already found.")
return
newpath = initial.rstrip(";")
if newpath:
newpath += ";"
newpath += str(Path(cmd.global_dir).absolute())
LOGGER.debug("New path: %s", newpath)
# Expand the value and ensure we are found
for p in os.path.expandvars(newpath).split(";"):
if not p:
continue
if p.casefold() == str(cmd.global_dir).casefold():
LOGGER.debug("Path is added successfully")
break
else:
return
with winreg.CreateKeyEx(winreg.HKEY_CURRENT_USER, "Environment",
access=winreg.KEY_READ|winreg.KEY_WRITE) as key:
initial2, kind2 = winreg.QueryValueEx(key, "Path")
if initial2 != initial or kind2 != kind:
LOGGER.debug("PATH has changed while we were working. Aborting.")
return
winreg.SetValueEx(key, "Path", 0, kind, newpath)
added = True
from _native import broadcast_settings_change
broadcast_settings_change()
notified = True
except Exception:
LOGGER.debug("Failed to update PATH environment variable", exc_info=True)
finally:
if added and not notified:
LOGGER.warn("Failed to notify of PATH environment variable change.")
LOGGER.info("You may need to sign out or restart to see the changes.")
elif not added:
LOGGER.error("Failed to update PATH environment variable successfully.")
LOGGER.info("You may add it yourself by opening 'Edit environment "
"variables' and adding this directory to 'PATH': !B!%s!W!",
cmd.global_dir)
else:
LOGGER.info("PATH has been updated, and will take effect after "
"opening a new terminal.")
def check_any_install(cmd):
LOGGER.debug("Checking for any Python runtime install")
if not cmd.get_installs(include_unmanaged=True, set_default=False):
LOGGER.debug("Check failed: no installs found")
return False
LOGGER.debug("Check passed: installs found")
return True
def _list_available_fallback_runtimes(cmd):
from .commands import find_command
candidates = []
try:
list_cmd = find_command(["list", "--online", "-1", "default"], cmd.root)
list_cmd.formatter_callable = lambda cmd, installs: candidates.extend(installs)
list_cmd.fallback_source_only = True
list_cmd.execute()
if not candidates:
list_cmd.fallback_source_only = False
list_cmd.execute()
except Exception:
LOGGER.debug("Check skipped: Failed to find 'list' command.", exc_info=True)
return []
except SystemExit:
LOGGER.debug("Check skipped: Failed to execute 'list' command.")
return []
return candidates
def check_latest_install(cmd):
LOGGER.debug("Checking if any default runtime is installed")
available = _list_available_fallback_runtimes(cmd)
if not available:
return "skip"
installs = cmd.get_installs(include_unmanaged=True, set_default=False)
if not installs:
LOGGER.debug("Check failed: no installs found")
return False
present = {i.get("tag") for i in installs}
available = set(j for i in available for j in i.get("install-for", []))
LOGGER.debug("Already installed: %s", sorted(present))
LOGGER.debug("Available: %s", sorted(available))
if available & present:
LOGGER.debug("Check passed: installs found")
return True
LOGGER.debug("Check failed: no equivalent 'default' runtime installed")
return False
def do_install(cmd):
from .commands import find_command
try:
inst_cmd = find_command(["install", "default", "--automatic"], cmd.root)
except Exception:
LOGGER.debug("Failed to find 'install' command.", exc_info=True)
LOGGER.warn("We couldn't install right now.")
LOGGER.info("Use !B!py install default!W! later to install.")
sys.exit(1)
else:
try:
inst_cmd.execute()
except Exception:
LOGGER.debug("Failed to run 'install' command.", exc_info=True)
raise
class _Welcome:
_shown = False
def __call__(self):
if not self._shown:
self._shown = True
LOGGER.print("!G!Welcome to the Python installation manager "
"configuration helper.!W!")
line_break()
def line_break():
LOGGER.print()
LOGGER.print("!B!" + "*" * logging.CONSOLE_MAX_WIDTH + "!W!")
LOGGER.print()
def first_run(cmd):
if not cmd.enabled:
return
shown_any = False
welcome = _Welcome()
if cmd.explicit:
welcome()
shown_any = True
if cmd.check_app_alias:
r = check_app_alias(cmd)
if not r:
welcome()
LOGGER.print("!Y!Your app execution alias settings are configured to launch "
"other commands besides 'py' and 'python'.!W!",
level=logging.WARN)
LOGGER.print("\nThis can be fixed by opening the '!B!Manage app "
"execution aliases!W!' settings page and enabling each "
"item labelled '!B!Python (default)!W!' and '!B!Python "
"install manager!W!'.\n", wrap=True)
LOGGER.print("If the items are already enabled, you may need to disable "
"and re-enable them.\n", wrap=True)
if (
cmd.confirm and
not cmd.ask_ny("Open Settings now, so you can modify !B!App "
"execution aliases!W!?")
):
os.startfile("ms-settings:advanced-apps")
LOGGER.print("\nThe Settings app should be open. Navigate to the "
"!B!App execution aliases!W! page and scroll to the "
"'!B!Python!W!' entries to enable the new commands.",
wrap=True)
line_break()
shown_any = True
elif cmd.explicit:
if r == "skip":
LOGGER.info("Skipped app execution aliases check")
else:
LOGGER.info("Checked app execution aliases")
line_break()
if cmd.check_long_paths:
if not check_long_paths(cmd):
welcome()
LOGGER.print("!Y!Windows is not configured to allow paths longer than "
"260 characters.!W!", level=logging.WARN)
LOGGER.print("\nPython and some other apps can exceed this limit, "
"but it requires changing a system-wide setting, which "
"may need an administrator to approve, and will require a "
"reboot. Some packages may fail to install without long "
"path support enabled.\n", wrap=True)
if not cmd.confirm or not cmd.ask_ny("Update setting now?"):
do_configure_long_paths(cmd)
line_break()
shown_any = True
elif cmd.explicit:
LOGGER.info("Checked system long paths setting")
line_break()
if cmd.check_py_on_path:
r = check_py_on_path(cmd)
if not r:
welcome()
LOGGER.print("!Y!The legacy 'py' command is still installed.!W!", level=logging.WARN)
LOGGER.print("\nThis may interfere with launching the new 'py' "
"command, and may be resolved by uninstalling "
"'!B!Python launcher!W!'.\n", wrap=True)
if cmd.confirm and not cmd.ask_ny("Open Installed apps now?"):
os.startfile("ms-settings:appsfeatures")
line_break()
shown_any = True
elif cmd.explicit:
if r == "skip":
LOGGER.info("Skipped check for legacy 'py' command")
else:
LOGGER.info("Checked PATH for legacy 'py' command")
line_break()
if cmd.check_global_dir:
r = check_global_dir(cmd)
if not r:
welcome()
LOGGER.print("!Y!The global shortcuts directory is not "
"configured.!W!", level=logging.WARN)
LOGGER.print("\nConfiguring this enables commands like "
"!B!python3.14.exe!W! to run from your terminal, "
"but is not needed for the !B!python!W! or !B!py!W! "
"commands (for example, !B!py -V:3.14!W!).",
wrap=True)
LOGGER.print("\nWe can add the directory (!B!%s!W!) to PATH now, "
"but you will need to restart your terminal to use "
"it. The entry will be removed if you run !B!py "
"uninstall --purge!W!, or else you can remove it "
"manually when uninstalling Python.\n", cmd.global_dir,
wrap=True)
if (
not cmd.confirm or
not cmd.ask_ny("Add commands directory to your PATH now?")
):
do_global_dir_on_path(cmd)
line_break()
shown_any = True
elif cmd.explicit:
if r == "skip":
LOGGER.info("Skipped check for commands directory on PATH")
else:
LOGGER.info("Checked PATH for versioned commands directory")
line_break()
# This check must be last, because a failed install may exit the program.
if cmd.check_any_install:
if not check_any_install(cmd):
welcome()
LOGGER.print("!Y!You do not have any Python runtimes installed.!W!",
level=logging.WARN)
LOGGER.print("\nInstall the current latest version of CPython? If "
"not, you can use '!B!py install default!W!' later to "
"install, or one will be installed automatically when "
"needed.\n", wrap=True)
LOGGER.info("")
if not cmd.confirm or cmd.ask_yn("Install CPython now?"):
do_install(cmd)
line_break()
shown_any = True
elif cmd.explicit:
LOGGER.info("Checked for any Python installs")
line_break()
if cmd.check_latest_install:
if not check_latest_install(cmd):
welcome()
LOGGER.print("!Y!You do not have the latest Python runtime.!W!",
level=logging.WARN)
LOGGER.print("\nInstall the current latest version of CPython? If "
"not, you can use '!B!py install default!W!' later to "
"install.\n", wrap=True)
LOGGER.info("")
if not cmd.confirm or cmd.ask_yn("Install CPython now?"):
do_install(cmd)
line_break()
shown_any = True
elif cmd.explicit:
LOGGER.info("Checked for the latest available Python install")
line_break()
if shown_any:
LOGGER.print("!G!Configuration checks completed.!W!", level=logging.WARN)
LOGGER.print("\nTo run these checks again, launch !B!Python install "
"manager!W! from your Start menu, or !B!py install "
"--configure!W! from the terminal.", wrap=True)
line_break()