diff --git a/.gitattributes b/.gitattributes index bf02a51..3623482 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ docs/** linguist-documentation +*.iss linguist-vendored diff --git a/.github/workflows/pyinstaller.yaml b/.github/workflows/pyinstaller.yaml index 4790567..2ff3708 100644 --- a/.github/workflows/pyinstaller.yaml +++ b/.github/workflows/pyinstaller.yaml @@ -114,11 +114,70 @@ jobs: run: | cat win-version.txt - - name: "Build" + # BEGIN - Windows Installer + + - name: "Windows Installer - Build" + if: ${{ matrix.os == 'windows-latest' }} shell: bash run: | uvx toml-run pyinstaller -- ${{ matrix.extra-args }} + - name: "Windows Installer - Download PathMgr" + if: ${{ matrix.os == 'windows-latest' }} + uses: robinraju/release-downloader@daf26c55d821e836577a15f77d86ddc078948b05 # v1.12 + with: + repository: "Bill-Stewart/PathMgr" + fileName: "PathMgr-2.0.0.zip" + out-file-path: "dist/PathMgr" + tag: "v2.0.0" + extract: true + + - name: "Windows Installer - Inno Setup" + env: + version: ${{ inputs.version }} + shell: pwsh + run: | + echo "Building Version: $Env:version" + iscc.exe "/DMyAppVersion=$Env:version" installer.iss + + - name: "Windows Installer - Debug Output" + if: ${{ matrix.os == 'windows-latest' }} + continue-on-error: true + shell: bash + run: | + echo "::group::dist" + ls -lAh dist/* + echo "::endgroup::" + echo "::group::out" + ls -lAhR out + echo "::endgroup::" + + - name: "Windows Installer - Upload Artifact" + if: ${{ matrix.os == 'windows-latest' && github.event_name != 'release' }} + uses: actions/upload-artifact@v5 + with: + name: windows-installer + path: out/*.exe + + - name: "Windows Installer - Upload Release" + if: ${{ matrix.os == 'windows-latest' && github.event_name == 'release' }} + uses: cssnr/upload-release-action@latest + with: + globs: out/*.exe + + - name: "Windows Installer - Cleanup" + if: ${{ matrix.os == 'windows-latest' }} + shell: bash + run: | + rm -rf dist out + + # END - Windows Installer + + - name: "Build" + shell: bash + run: | + uvx toml-run pyinstaller -- -F ${{ matrix.extra-args }} + - name: "List Artifacts" continue-on-error: true working-directory: dist diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index b08dfcc..64d5661 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -162,3 +162,20 @@ jobs: with: webhook: ${{ secrets.DISCORD_WEBHOOK }} description: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + + virustotal: + name: "VirusTotal Scan" + runs-on: ubuntu-latest + needs: [pyinstaller, release] + timeout-minutes: 5 + if: ${{ github.event_name == 'release' }} + + steps: + - name: "VirusTotal" + uses: cssnr/virustotal-action@master + continue-on-error: true + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + vt_api_key: ${{ secrets.VT_API_KEY }} + update_release: true + collapsed: true diff --git a/.gitignore b/.gitignore index 4039fdc..f8e497f 100644 --- a/.gitignore +++ b/.gitignore @@ -2,14 +2,15 @@ .idea/ *.iml .vscode/ +.*cache* +node_modules/ +build/ +dist/ +out/ .venv/ venv/ __pycache__/ *.egg-info/ -build/ -dist/ -.*cache/ -*.log *.pyc .coverage coverage.xml diff --git a/assets/post-install.rtf b/assets/post-install.rtf new file mode 100644 index 0000000..d6ff418 Binary files /dev/null and b/assets/post-install.rtf differ diff --git a/assets/pre-install.rtf b/assets/pre-install.rtf new file mode 100644 index 0000000..e28d783 Binary files /dev/null and b/assets/pre-install.rtf differ diff --git a/installer.iss b/installer.iss new file mode 100644 index 0000000..bbe0b10 --- /dev/null +++ b/installer.iss @@ -0,0 +1,168 @@ +#define MyAppName "ShareX CLI" +#define MyAppPublisher "CSSNR" +#define MyAppURL "https://cssnr.github.io/sharex-cli/" +#define MyAppExeName "sharex.exe" +#ifndef MyAppVersion + #define MyAppVersion "0.0.1" +#endif + +[Setup] +AppId={{1FD9B94A-D2D2-40FA-81D5-A4B5BCD39A47} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +;Compression=lzma +;SolidCompression=yes +DefaultDirName={autopf}\sharex-cli +DefaultGroupName={#MyAppName} +;DisableDirPage=yes +;DisableProgramGroupPage=yes +DisableFinishedPage=yes +InfoBeforeFile=assets\pre-install.rtf +InfoAfterFile=assets\post-install.rtf + +OutputBaseFilename=windows-installer +OutputDir=out +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +SetupIconFile=docs\favicon.ico +UninstallDisplayIcon={uninstallexe} +WizardStyle=modern dynamic + +ChangesEnvironment=yes +;LicenseFile=LICENSE +;VersionInfoVersion={#MyAppVersion} + +; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run +; on anything but x64 and Windows 11 on Arm. +;ArchitecturesAllowed=x64compatible +; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the +; install be done in "64-bit mode" on x64 or Windows 11 on Arm, +; meaning it should use the native 64-bit Program Files directory and +; the 64-bit view of the registry. +ArchitecturesInstallIn64BitMode=x64compatible + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Files] +Source: "dist\PathMgr\i386\PathMgr.dll"; DestDir: "{app}"; Flags: uninsneveruninstall +;Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion +Source: "dist\sharex\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + +[Icons] +Name: "{group}\{#MyAppName} Folder"; Filename: "{app}" +Name: "{group}\{#MyAppName} Documentation"; Filename: "{#MyAppURL}" +Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" + +[Tasks] +Name: modifypath; Description: "&Add to Path" + +[Code] +const + MODIFY_PATH_TASK_NAME = 'modifypath'; // Specify name of task + +var + PathIsModified: Boolean; // Cache task selection from previous installs + ApplicationUninstalled: Boolean; // Has application been uninstalled? + +// Import AddDirToPath() at setup time ('files:' prefix) +function DLLAddDirToPath(DirName: string; PathType, AddType: DWORD): DWORD; + external 'AddDirToPath@files:PathMgr.dll stdcall setuponly'; + +// Import RemoveDirFromPath() at uninstall time ('{app}\' prefix) +function DLLRemoveDirFromPath(DirName: string; PathType: DWORD): DWORD; + external 'RemoveDirFromPath@{app}\PathMgr.dll stdcall uninstallonly'; + +// Wrapper for AddDirToPath() DLL function +function AddDirToPath(const DirName: string): DWORD; +var + PathType, AddType: DWORD; +begin + // PathType = 0 - use system Path + // PathType = 1 - use user Path + // AddType = 0 - add to end of Path + // AddType = 1 - add to beginning of Path + if IsAdminInstallMode() then + PathType := 0 + else + PathType := 1; + AddType := 0; + result := DLLAddDirToPath(DirName, PathType, AddType); +end; + +// Wrapper for RemoveDirFromPath() DLL function +function RemoveDirFromPath(const DirName: string): DWORD; +var + PathType: DWORD; +begin + // PathType = 0 - use system Path + // PathType = 1 - use user Path + if IsAdminInstallMode() then + PathType := 0 + else + PathType := 1; + result := DLLRemoveDirFromPath(DirName, PathType); +end; + +procedure RegisterPreviousData(PreviousDataKey: Integer); +begin + // Store previous or current task selection as custom user setting + if PathIsModified or WizardIsTaskSelected(MODIFY_PATH_TASK_NAME) then + SetPreviousData(PreviousDataKey, MODIFY_PATH_TASK_NAME, 'true'); +end; + +function InitializeSetup(): Boolean; +begin + result := true; + // Was task selected during a previous install? + PathIsModified := GetPreviousData(MODIFY_PATH_TASK_NAME, '') = 'true'; +end; + +function InitializeUninstall(): Boolean; +begin + result := true; + // Was task selected during a previous install? + PathIsModified := GetPreviousData(MODIFY_PATH_TASK_NAME, '') = 'true'; + ApplicationUninstalled := false; +end; + +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssPostInstall then + begin + // Add app directory to Path at post-install step if task selected + if PathIsModified or WizardIsTaskSelected(MODIFY_PATH_TASK_NAME) then + AddDirToPath(ExpandConstant('{app}')); + end; +end; + +procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); +begin + if CurUninstallStep = usUninstall then + begin + // Remove app directory from path during uninstall if task was selected; + // use variable because we can't use WizardIsTaskSelected() at uninstall + if PathIsModified then + RemoveDirFromPath(ExpandConstant('{app}')); + end + else if CurUninstallStep = usPostUninstall then + begin + ApplicationUninstalled := true; + end; +end; + +procedure DeinitializeUninstall(); +begin + if ApplicationUninstalled then + begin + // Unload and delete PathMgr.dll and remove app dir when uninstalling + UnloadDLL(ExpandConstant('{app}\PathMgr.dll')); + DeleteFile(ExpandConstant('{app}\PathMgr.dll')); + RemoveDir(ExpandConstant('{app}')); + end; +end; diff --git a/pyproject.toml b/pyproject.toml index 75679e8..07e159d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -123,7 +123,13 @@ select = ["E4", "E7", "E9", "F", "B", "Q"] [tool.scripts] win-version = "uv run pyivf-make_version --source-format yaml --metadata-source .github/files/win-version.yaml --outfile win-version.txt" -pyinstaller = "uv run pyinstaller -F -n sharex -i docs/favicon.ico src/app.py" +pyinstaller = "uv run pyinstaller -n sharex -i docs/favicon.ico src/app.py" +pathmgr = [ + "mkdir dist || echo dist dir exists", + "wget https://github.com/Bill-Stewart/PathMgr/releases/download/v2.0.0/PathMgr-2.0.0.zip -O dist/pathmgr.zip", + "unzip dist/pathmgr.zip -d dist/PathMgr", +] +inno = ["iscc.exe installer.iss"] test = ["uv run coverage run -m pytest", "uv run coverage report -m"] clean = "rm -rf .cache dist site" build = "uv run hatch build"