Skip to content

Commit 2f38121

Browse files
committed
[WIP] Add 'first run' configuration experience to prompt users to modify system settings.
Fixes #93
1 parent 9a150b3 commit 2f38121

File tree

6 files changed

+386
-12
lines changed

6 files changed

+386
-12
lines changed

_msbuild.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ class ResourceFile(CSourceFile):
8484
CFunction('date_as_str'),
8585
CFunction('datetime_as_str'),
8686
CFunction('reg_rename_key'),
87+
CFunction('read_alias_package'),
8788
source='src/_native',
8889
RootNamespace='_native',
8990
)

_msbuild_test.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
CFunction('date_as_str'),
5151
CFunction('datetime_as_str'),
5252
CFunction('reg_rename_key'),
53+
CFunction('read_alias_package'),
5354
source='src/_native',
5455
),
5556
DllPackage('_shellext_test',

src/_native/misc.cpp

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,4 +119,39 @@ PyObject *reg_rename_key(PyObject *, PyObject *args, PyObject *kwargs) {
119119
return r;
120120
}
121121

122+
123+
PyObject *read_alias_package(PyObject *, PyObject *args, PyObject *kwargs) {
124+
static const char * keywords[] = {"path", NULL};
125+
wchar_t *path = NULL;
126+
if (!PyArg_ParseTupleAndKeywords(args, kwargs, "O&:read_alias_package", keywords,
127+
as_utf16, &path)) {
128+
return NULL;
129+
}
130+
131+
HANDLE h = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING,
132+
FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, NULL);
133+
PyMem_Free(path);
134+
if (h == INVALID_HANDLE_VALUE) {
135+
PyErr_SetFromWindowsErr(0);
136+
return NULL;
137+
}
138+
139+
wchar_t buffer[32768];
140+
DWORD nread;
141+
142+
if (!DeviceIoControl(h, FSCTL_GET_REPARSE_POINT, NULL, 0,
143+
buffer, sizeof(buffer), &nread, NULL)) {
144+
PyErr_SetFromWindowsErr(0);
145+
CloseHandle(h);
146+
return NULL;
147+
}
148+
CloseHandle(h);
149+
150+
if (*(DWORD*)buffer != IO_REPARSE_TAG_APPEXECLINK) {
151+
return Py_GetConstant(Py_CONSTANT_NONE);
152+
}
153+
154+
return PyUnicode_FromWideChar(&buffer[4], nread / sizeof(wchar_t) - 5);
155+
}
156+
122157
}

src/manage/commands.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,19 @@
2525
DEFAULT_TAG = "3.14"
2626

2727

28+
# TODO: Remove the /dev/ for stable release
29+
HELP_URL = "https://docs.python.org/dev/using/windows"
30+
31+
2832
COPYRIGHT = f"""Python installation manager {__version__}
2933
Copyright (c) Python Software Foundation. All Rights Reserved.
3034
"""
3135

3236

37+
if EXE_NAME.casefold() == "py-manager".casefold():
38+
EXE_NAME = "py"
39+
40+
3341
WELCOME = f"""!B!Python install manager was successfully updated to {__version__}.!W!
3442
"""
3543

@@ -188,6 +196,7 @@ def execute(self):
188196
"enable-shortcut-kinds": ("enable_shortcut_kinds", _NEXT, config_split),
189197
"disable-shortcut-kinds": ("disable_shortcut_kinds", _NEXT, config_split),
190198
"help": ("show_help", True), # nested to avoid conflict with command
199+
"configure": ("configure", True),
191200
# Set when the manager is doing an automatic install.
192201
# Generally won't be set by manual invocation
193202
"automatic": ("automatic", True),
@@ -202,6 +211,10 @@ def execute(self):
202211
"force": ("confirm", False),
203212
"help": ("show_help", True), # nested to avoid conflict with command
204213
},
214+
215+
"**first_run": {
216+
"explicit": ("explicit", True),
217+
},
205218
}
206219

207220

@@ -240,6 +253,17 @@ def execute(self):
240253
"disable_shortcut_kinds": (str, config_split_append),
241254
},
242255

256+
"first_run": {
257+
"enabled": (config_bool, None, "env"),
258+
"explicit": (config_bool, None),
259+
"check_app_alias": (config_bool, None, "env"),
260+
"check_long_paths": (config_bool, None, "env"),
261+
"check_py_on_path": (config_bool, None, "env"),
262+
"check_any_install": (config_bool, None, "env"),
263+
"check_default_tag": (config_bool, None, "env"),
264+
"check_global_dir": (config_bool, None, "env"),
265+
},
266+
243267
# These configuration settings are intended for administrative override only
244268
# For example, if you are managing deployments that will use your own index
245269
# and/or your own builds.
@@ -419,11 +443,11 @@ def __init__(self, args, root=None):
419443
# If our command has any config, load them to override anything that
420444
# wasn't set on the command line.
421445
try:
422-
cmd_config = config[self.CMD]
446+
cmd_config = config[self.CMD.lstrip("*")]
423447
except (AttributeError, LookupError):
424448
pass
425449
else:
426-
arg_names = frozenset(CONFIG_SCHEMA[self.CMD])
450+
arg_names = frozenset(CONFIG_SCHEMA[self.CMD.lstrip("*")])
427451
for k, v in cmd_config.items():
428452
if k in arg_names and k not in _set_args:
429453
LOGGER.debug("Overriding command option %s with %r", k, v)
@@ -511,7 +535,7 @@ def get_log_file(self):
511535
logs_dir = Path(os.getenv("TMP") or os.getenv("TEMP") or os.getcwd())
512536
from _native import datetime_as_str
513537
self._log_file = logs_dir / "python_{}_{}_{}.log".format(
514-
self.CMD, datetime_as_str(), os.getpid()
538+
self.CMD.strip("*"), datetime_as_str(), os.getpid()
515539
)
516540
return self._log_file
517541

@@ -564,8 +588,7 @@ def show_usage(cls):
564588
LOGGER.print(r)
565589

566590
LOGGER.print()
567-
# TODO: Remove the /dev/ for stable release
568-
LOGGER.print("Find additional information at !B!https://docs.python.org/dev/using/windows!W!.")
591+
LOGGER.print("Find additional information at !B!%s!W!.", HELP_URL)
569592
LOGGER.print()
570593

571594
@classmethod
@@ -746,6 +769,7 @@ class InstallCommand(BaseCommand):
746769
-u, --update Overwrite existing install if a newer version is available.
747770
--dry-run Choose runtime but do not install
748771
--refresh Update shortcuts and aliases for all installed versions.
772+
--configure Re-run the system configuration helper.
749773
--by-id Require TAG to exactly match the install ID. (For advanced use.)
750774
!B!<TAG> <TAG>!W! ... One or more tags to install (Company\Tag format)
751775
@@ -775,6 +799,7 @@ class InstallCommand(BaseCommand):
775799
dry_run = False
776800
refresh = False
777801
by_id = False
802+
configure = False
778803
automatic = False
779804
from_script = None
780805
enable_shortcut_kinds = None
@@ -801,9 +826,13 @@ def __init__(self, args, root=None):
801826
self.download = Path(self.download).absolute()
802827

803828
def execute(self):
804-
from .install_command import execute
805829
self.show_welcome()
806-
execute(self)
830+
if self.configure:
831+
cmd = FirstRun(["**first_run", "--explicit"], self.root)
832+
cmd.execute()
833+
else:
834+
from .install_command import execute
835+
execute(self)
807836

808837

809838
class UninstallCommand(BaseCommand):
@@ -891,7 +920,7 @@ def execute(self):
891920

892921

893922
class HelpWithErrorCommand(HelpCommand):
894-
CMD = "__help_with_error"
923+
CMD = "**help_with_error"
895924

896925
def __init__(self, args, root=None):
897926
# Essentially disable argument processing for this command
@@ -945,6 +974,29 @@ def __init__(self, root):
945974
super().__init__([], root)
946975

947976

977+
class FirstRun(BaseCommand):
978+
CMD = "**first_run"
979+
enabled = True
980+
explicit = False
981+
check_app_alias = True
982+
check_long_paths = True
983+
check_py_on_path = True
984+
check_any_install = True
985+
check_default_tag = True
986+
check_global_dir = True
987+
988+
def execute(self):
989+
if not self.enabled:
990+
return
991+
from .firstrun import first_run
992+
first_run(self)
993+
if not self.explicit:
994+
show_help([])
995+
if self.confirm and not self.ask_ny(f"View more help online? (!B!{HELP_URL}!W!)"):
996+
import os
997+
os.startfile(HELP_URL)
998+
999+
9481000
def load_default_config(root):
9491001
return DefaultConfig(root)
9501002

0 commit comments

Comments
 (0)