diff --git a/CLIENT_AGENT_V2_INSTALLER.md b/CLIENT_AGENT_V2_INSTALLER.md index 1e038ca..0283172 100644 --- a/CLIENT_AGENT_V2_INSTALLER.md +++ b/CLIENT_AGENT_V2_INSTALLER.md @@ -47,6 +47,12 @@ build_client_agent_v2_installer_windows.bat dist\Inv_web_Client_Setup.exe ``` +При сборке клиент формируется в формате `onedir` и целиком включается в Inno +Setup. Это исключает распаковку Tcl/Tk во временный каталог `_MEI...` при запуске. +Сценарий сборки отдельно проверяет наличие `_tcl_data\init.tcl` и +`_tk_data\tk.tcl`, а затем запускает клиент с `--help` как smoke-тест. Поэтому +установщик не будет создан с неполным или не запускающимся Tkinter. + Для тихой установки и настройки службы RustDesk мастер запрашивает права администратора. Клиент устанавливается в каталог: diff --git a/build_client_agent_v2_installer_windows.bat b/build_client_agent_v2_installer_windows.bat index 4bf9ff7..6b4af5d 100644 --- a/build_client_agent_v2_installer_windows.bat +++ b/build_client_agent_v2_installer_windows.bat @@ -3,7 +3,8 @@ setlocal cd /d "%~dp0" set "SPEC_FILE=client_agent_v2_windows_onefile.spec" -set "CLIENT_EXE=dist\Inv_web Client.exe" +set "CLIENT_DIR=dist\Inv_web Client" +set "CLIENT_EXE=%CLIENT_DIR%\Inv_web Client.exe" set "INSTALLER_SCRIPT=client_agent_v2_installer.iss" set "RUSTDESK_INSTALLER=installer\prerequisites\rustdesk.exe" set "ISCC=%ProgramFiles(x86)%\Inno Setup 6\ISCC.exe" @@ -55,9 +56,33 @@ if not exist "%ISCC%" ( echo Inno Setup compiler: %ISCC% echo Installing Python build dependencies... -py -m pip install --upgrade pyinstaller pystray pillow +py -m pip install --upgrade "pyinstaller>=6.18" pystray pillow if errorlevel 1 exit /b 1 +set "PYTHON_BASE=" +for /f "delims=" %%I in ('py -c "import sys; print(sys.base_prefix)"') do set "PYTHON_BASE=%%I" +if not defined PYTHON_BASE ( + echo Could not determine the Python installation directory. + exit /b 1 +) + +set "TCL_LIBRARY=" +for /d %%D in ("%PYTHON_BASE%\tcl\tcl*") do if exist "%%~fD\init.tcl" set "TCL_LIBRARY=%%~fD" +set "TK_LIBRARY=" +for /d %%D in ("%PYTHON_BASE%\tcl\tk*") do if exist "%%~fD\tk.tcl" set "TK_LIBRARY=%%~fD" + +if not defined TCL_LIBRARY ( + echo Tcl runtime was not found under: %PYTHON_BASE%\tcl + exit /b 1 +) +if not defined TK_LIBRARY ( + echo Tk runtime was not found under: %PYTHON_BASE%\tcl + exit /b 1 +) + +echo Tcl runtime: %TCL_LIBRARY% +echo Tk runtime: %TK_LIBRARY% + echo Building client executable... py -m PyInstaller --clean --noconfirm "%SPEC_FILE%" if errorlevel 1 exit /b 1 @@ -67,6 +92,23 @@ if not exist "%CLIENT_EXE%" ( exit /b 1 ) +if not exist "%CLIENT_DIR%\_internal\_tcl_data\init.tcl" ( + echo Tcl data was not included in the client build. + exit /b 1 +) + +if not exist "%CLIENT_DIR%\_internal\_tk_data\tk.tcl" ( + echo Tk data was not included in the client build. + exit /b 1 +) + +echo Testing the packaged client runtime... +start "" /wait "%CLIENT_EXE%" --help +if errorlevel 1 ( + echo The packaged client failed its startup test. + exit /b 1 +) + echo Building Windows installer... "%ISCC%" "%INSTALLER_SCRIPT%" if errorlevel 1 exit /b 1 diff --git a/client_agent_v2_installer.iss b/client_agent_v2_installer.iss index 52a5492..3bb4de5 100644 --- a/client_agent_v2_installer.iss +++ b/client_agent_v2_installer.iss @@ -31,7 +31,7 @@ DisableReadyPage=yes Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl" [Files] -Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "dist\Inv_web Client\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs Source: "{#RustDeskInstaller}"; DestDir: "{tmp}"; DestName: "rustdesk-installer.exe"; Flags: deleteafterinstall noencryption; Check: ShouldInstallRustDesk Source: "installer\configure_rustdesk.ps1"; DestDir: "{tmp}"; Flags: deleteafterinstall noencryption; Check: ShouldInstallRustDesk diff --git a/client_agent_v2_windows_onefile.spec b/client_agent_v2_windows_onefile.spec index c58e20d..38fcdd8 100644 --- a/client_agent_v2_windows_onefile.spec +++ b/client_agent_v2_windows_onefile.spec @@ -1,6 +1,8 @@ # -*- mode: python ; coding: utf-8 -*- import os +import sys +from pathlib import Path project_root = ( os.path.abspath(os.path.dirname(__file__)) @@ -8,15 +10,44 @@ project_root = ( else os.path.abspath(os.getcwd()) ) + +def find_tcl_runtime_directory(environment_name, directory_prefix, marker_file): + configured_path = os.environ.get(environment_name) + if configured_path and os.path.isfile(os.path.join(configured_path, marker_file)): + return os.path.abspath(configured_path) + + tcl_root = Path(sys.base_prefix) / "tcl" + for candidate in sorted(tcl_root.glob(f"{directory_prefix}*"), reverse=True): + if (candidate / marker_file).is_file(): + return str(candidate.resolve()) + + return None + + +datas = [ + (os.path.join(project_root, "picture", "ICON", "icon_client.png"), "picture/ICON"), + (os.path.join(project_root, "picture", "Client", "Connect", "connected.png"), "picture/Client/Connect"), + (os.path.join(project_root, "picture", "Client", "Connect", "no-connection.png"), "picture/Client/Connect"), +] + +if os.name == "nt": + tcl_library = find_tcl_runtime_directory("TCL_LIBRARY", "tcl", "init.tcl") + tk_library = find_tcl_runtime_directory("TK_LIBRARY", "tk", "tk.tcl") + if not tcl_library or not tk_library: + raise RuntimeError( + "Tcl/Tk data directories were not found. " + "Install Python with Tcl/Tk support or set TCL_LIBRARY and TK_LIBRARY." + ) + datas.extend([ + (tcl_library, "_tcl_data"), + (tk_library, "_tk_data"), + ]) + a = Analysis( ["client_agent_v2.py"], pathex=[project_root], binaries=[], - datas=[ - (os.path.join(project_root, "picture", "ICON", "icon_client.png"), "picture/ICON"), - (os.path.join(project_root, "picture", "Client", "Connect", "connected.png"), "picture/Client/Connect"), - (os.path.join(project_root, "picture", "Client", "Connect", "no-connection.png"), "picture/Client/Connect"), - ], + datas=datas, hiddenimports=["pystray", "pystray._win32", "PIL.Image", "PIL.ImageTk"], hookspath=[], hooksconfig={}, @@ -32,15 +63,13 @@ pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, - a.binaries, - a.zipfiles, - a.datas, [], + exclude_binaries=True, name="Inv_web Client", debug=False, bootloader_ignore_signals=False, strip=False, - upx=True, + upx=False, console=False, disable_windowed_traceback=False, argv_emulation=False, @@ -49,3 +78,13 @@ exe = EXE( entitlements_file=None, icon=os.path.join(project_root, "picture", "ICON", "icon_client.ico"), ) + +coll = COLLECT( + exe, + a.binaries, + a.datas, + strip=False, + upx=False, + upx_exclude=[], + name="Inv_web Client", +)