diff --git a/.gitignore b/.gitignore index 06f7035..c8f556c 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,8 @@ *.egg-info /packaging/innosetup/.extensions +/packaging/choco/*/*-generated +/packaging/choco/*/*.generated.nuspec # Caches __pycache__ diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 810b888..67aa9fb 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,10 +1,11 @@ { - "recommendations": [ - "chouzz.vscode-innosetup", - "davidanson.vscode-markdownlint", - "ms-python.debugpy", - "ms-python.python", - "ms-python.vscode-pylance", - "charliermarsh.ruff" - ] + "recommendations": [ + "chouzz.vscode-innosetup", + "davidanson.vscode-markdownlint", + "ms-python.debugpy", + "ms-python.python", + "ms-python.vscode-pylance", + "charliermarsh.ruff", + "ms-vscode.powershell" + ] } \ No newline at end of file diff --git a/README_choco.md b/README_choco.md new file mode 100644 index 0000000..9bf0234 --- /dev/null +++ b/README_choco.md @@ -0,0 +1,34 @@ +VCF Generator Lite can convert a contact list into a single vCard file, which can be batch-imported into mobile phone +contacts or used for other purposes. + +## Features + +- **Smart Parsing**: Batch contacts in `Name Phone Note` format (note optional), automatically handles tabs and spaces. +- **Batch Generation**: Combines all contacts into a single `.vcf` file. +- **Number Validation**: Automatically skips invalid numbers and quickly locates error rows. +- **Editing Assistance**: Displays line numbers in text area, supports one-click quote removal. + +## Usage + +1. Copy the name and phone number in the format of `Name Phone Note` on each line into the text field below. The note + can be omitted. + ```text + Isaac Newton 13445467890 British mathematician + Muhammad 13554678907 + Confucius 13645436748 + ``` +2. Click **Generate**, select a path to save the file. +3. You can use the generated vCard file wherever you need it. + +> [!NOTE] +> +> - You can use both tabs and spaces to separate the name and phone number. +> - The program will automatically remove extra spaces from the text field. +> +> For example, ` Han Meimei 13333333333 A well-known girl` will be recognized as +> +> +> > - Name: Han Meimei +> > - Phone: 13333333333 +> > - Note: A well-known girl +> diff --git a/packaging/choco/installable/tools-template/LICENSE.txt b/packaging/choco/installable/tools-template/LICENSE.txt new file mode 100644 index 0000000..97285cb --- /dev/null +++ b/packaging/choco/installable/tools-template/LICENSE.txt @@ -0,0 +1,5 @@ +From: $dynamic:{license_url} + +LICENSE + +$dynamic:{license_content} diff --git a/packaging/choco/installable/tools-template/VERIFICATION.txt b/packaging/choco/installable/tools-template/VERIFICATION.txt new file mode 100644 index 0000000..701ef0a --- /dev/null +++ b/packaging/choco/installable/tools-template/VERIFICATION.txt @@ -0,0 +1,8 @@ +VERIFICATION +Verification is intended to assist the Chocolatey moderators and community +in verifying that this package's contents are trustworthy. + +1. Download the following file from https://gitee.com/hellotool/VCFGeneratorLiteWithTkinter/releases/v$dynamic:{version_wheel}: + - $dynamic:{installer_filename} +2. Use any tool to verify the file information. + - SHA 256: $dynamic:{installer_sha256} \ No newline at end of file diff --git a/packaging/choco/installable/tools-template/chocolateyinstall.ps1 b/packaging/choco/installable/tools-template/chocolateyinstall.ps1 new file mode 100644 index 0000000..73f6606 --- /dev/null +++ b/packaging/choco/installable/tools-template/chocolateyinstall.ps1 @@ -0,0 +1,18 @@ +$ErrorActionPreference = 'Stop' +$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" +$fileLocation = Join-Path $toolsDir '$dynamic:{installer_filename}' + + +$packageArgs = @{ + packageName = $env:ChocolateyPackageName + unzipLocation = $toolsDir + fileType = 'exe' + file = $fileLocation + checksum64 = '$dynamic:{installer_sha256}' + checksumType64 = 'sha256' + silentArgs = '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-' # Inno Setup + validExitCodes = @(0) +} + +Install-ChocolateyInstallPackage @packageArgs +Remove-Item $toolsDir\*.exe -ea 0 -force diff --git a/packaging/choco/installable/tools/chocolateybeforemodify.ps1 b/packaging/choco/installable/tools/chocolateybeforemodify.ps1 new file mode 100644 index 0000000..5f28270 --- /dev/null +++ b/packaging/choco/installable/tools/chocolateybeforemodify.ps1 @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packaging/choco/installable/tools/chocolateyuninstall.ps1 b/packaging/choco/installable/tools/chocolateyuninstall.ps1 new file mode 100644 index 0000000..4f39ed4 --- /dev/null +++ b/packaging/choco/installable/tools/chocolateyuninstall.ps1 @@ -0,0 +1,44 @@ +# IMPORTANT: Before releasing this package, copy/paste the next 2 lines into PowerShell to remove all comments from this file: +# $f='c:\path\to\thisFile.ps1' +# gc $f | ? {$_ -notmatch "^\s*#"} | % {$_ -replace '(^.*?)\s*?[^``]#.*','$1'} | Out-File $f+".~" -en utf8; mv -fo $f+".~" $f + +## NOTE: In 80-90% of the cases (95% with licensed versions due to Package Synchronizer and other enhancements), +## AutoUninstaller should be able to detect and handle registry uninstalls without a chocolateyUninstall.ps1. +## See https://docs.chocolatey.org/en-us/choco/commands/uninstall +## and https://docs.chocolatey.org/en-us/create/functions/uninstall-chocolateypackage + +## If this is an MSI, ensure 'softwareName' is appropriate, then clean up comments and you are done. +## If this is an exe, change fileType, silentArgs, and validExitCodes + +$ErrorActionPreference = 'Stop' # stop on all errors +$packageArgs = @{ + packageName = $env:ChocolateyPackageName + softwareNames = @('VCF 生成器 Lite', 'VCF Generator Lite') + fileType = 'EXE' + silentArgs = '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-' + validExitCodes = @(0) +} + +$foundKeys = @() +foreach ($softwareName in $packageArgs['softwareNames']) { + [array]$keys = Get-UninstallRegistryKey -SoftwareName $softwareName + if ($keys.Count -gt 0) { + $foundKeys += $keys + } +} + +if ($foundKeys.Count -eq 1) { + $foundKeys | ForEach-Object { + $packageArgs['file'] = "$($_.UninstallString)" #NOTE: You may need to split this if it contains spaces, see below + Uninstall-ChocolateyPackage @packageArgs + } +} +elseif ($foundKeys.Count -eq 0) { + Write-Warning "$packageName has already been uninstalled by other means." +} +elseif ($foundKeys.Count -gt 1) { + Write-Warning "$($foundKeys.Count) matches found!" + Write-Warning "To prevent accidental data loss, no programs will be uninstalled." + Write-Warning "Please alert package maintainer the following keys were matched:" + $foundKeys | ForEach-Object { Write-Warning "- $($_.DisplayName)" } +} diff --git a/packaging/choco/installable/vcf-generator-lite.install.template.nuspec b/packaging/choco/installable/vcf-generator-lite.install.template.nuspec new file mode 100644 index 0000000..76f377f --- /dev/null +++ b/packaging/choco/installable/vcf-generator-lite.install.template.nuspec @@ -0,0 +1,35 @@ + + + + + + vcf-generator-lite.install + $dynamic:{version_windows} + $dynamic:{repository_url} + $dynamic:{author} + + + + $dynamic:{display_name} (Install) + $dynamic:{author} + $dynamic:{repository_url} + $dynamic:{icon_url_cdn} + $dynamic:{copyright} + $dynamic:{license_url} + true + $dynamic:{repository_url} + $dynamic:{bug_tracker_url} + vcf-generator make-vcf vcf vcard-generator make-vcard vcard contacts + $dynamic:{summary} + $dynamic:{description} + + + + + + + + + + \ No newline at end of file diff --git a/packaging/choco/meta/tools/chocolateybeforemodify.ps1 b/packaging/choco/meta/tools/chocolateybeforemodify.ps1 new file mode 100644 index 0000000..eadc55f --- /dev/null +++ b/packaging/choco/meta/tools/chocolateybeforemodify.ps1 @@ -0,0 +1,9 @@ +# This runs before upgrade or uninstall. +# Use this file to do things like stop services prior to upgrade or uninstall. +# NOTE: It is an anti-pattern to call chocolateyUninstall.ps1 from here. If you +# need to uninstall an MSI prior to upgrade, put the functionality in this +# file without calling the uninstall script. Make it idempotent in the +# uninstall script so that it doesn't fail when it is already uninstalled. +# NOTE: For upgrades - like the uninstall script, this script always runs from +# the currently installed version, not from the new upgraded package version. + diff --git a/packaging/choco/meta/tools/chocolateyinstall.ps1 b/packaging/choco/meta/tools/chocolateyinstall.ps1 new file mode 100644 index 0000000..40675d1 --- /dev/null +++ b/packaging/choco/meta/tools/chocolateyinstall.ps1 @@ -0,0 +1,136 @@ +# IMPORTANT: Before releasing this package, copy/paste the next 2 lines into PowerShell to remove all comments from this file: +# $f='c:\path\to\thisFile.ps1' +# gc $f | ? {$_ -notmatch "^\s*#"} | % {$_ -replace '(^.*?)\s*?[^``]#.*','$1'} | Out-File $f+".~" -en utf8; mv -fo $f+".~" $f + +$ErrorActionPreference = 'Stop' # stop on all errors +$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" +# Internal packages (organizations) or software that has redistribution rights (community repo) +# - Use `Install-ChocolateyInstallPackage` instead of `Install-ChocolateyPackage` +# and put the binaries directly into the tools folder (we call it embedding) +$fileLocation = Join-Path $toolsDir '{installer_filename}' +# If embedding binaries increase total nupkg size to over 1GB, use share location or download from urls +#$fileLocation = '\\SHARE_LOCATION\to\INSTALLER_FILE' +# Community Repo: Use official urls for non-redist binaries or redist where total package size is over 200MB +# Internal/Organization: Download from internal location (internet sources are unreliable) +$url = '' # download url, HTTPS preferred +$url64 = '' # 64bit URL here (HTTPS preferred) or remove - if installer contains both (very rare), use $url + +$packageArgs = @{ + packageName = $env:ChocolateyPackageName + unzipLocation = $toolsDir + fileType = 'EXE_MSI_OR_MSU' #only one of these: exe, msi, msu + url = $url + url64bit = $url64 + file = $fileLocation + + softwareName = 'vcf-generator-lite*' #part or all of the Display Name as you see it in Programs and Features. It should be enough to be unique + + # Checksums are required for packages which will be hosted on the Chocolatey Community Repository. + # To determine checksums, you can get that from the original site if provided. + # You can also use checksum.exe (choco install checksum) and use it + # e.g. checksum -t sha256 -f path\to\file + checksum = '' + checksumType = 'sha256' #default is md5, can also be sha1, sha256 or sha512 + checksum64 = '' + checksumType64= 'sha256' #default is checksumType + + # MSI +# silentArgs = "/qn /norestart /l*v `"$($env:TEMP)\$($packageName).$($env:chocolateyPackageVersion).MsiInstall.log`"" # ALLUSERS=1 DISABLEDESKTOPSHORTCUT=1 ADDDESKTOPICON=0 ADDSTARTMENU=0 + validExitCodes= @(0, 3010, 1641) + # OTHERS + # Uncomment matching EXE type (sorted by most to least common) + #silentArgs = '/S' # NSIS + #silentArgs = '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-' # Inno Setup + #silentArgs = '/s' # InstallShield + #silentArgs = '/s /v"/qn"' # InstallShield with MSI + #silentArgs = '/s' # Wise InstallMaster + #silentArgs = '-s' # Squirrel + #silentArgs = '-q' # Install4j + #silentArgs = '-s' # Ghost + # Note that some installers, in addition to the silentArgs above, may also need assistance of AHK to achieve silence. + #silentArgs = '' # none; make silent with input macro script like AutoHotKey (AHK) + # https://community.chocolatey.org/packages/autohotkey.portable + #validExitCodes= @(0) #please insert other valid exit codes here +} + +# Install-ChocolateyPackage @packageArgs # https://docs.chocolatey.org/en-us/create/functions/install-chocolateypackage +#Install-ChocolateyZipPackage @packageArgs # https://docs.chocolatey.org/en-us/create/functions/install-chocolateyzippackage +## If you are making your own internal packages (organizations), you can embed the installer or +## put on internal file share and use the following instead (you'll need to add $file to the above) +# Install-ChocolateyInstallPackage @packageArgs # https://docs.chocolatey.org/en-us/create/functions/install-chocolateyinstallpackage + +## Main helper functions - these have error handling tucked into them already +## see https://docs.chocolatey.org/en-us/create/functions + +## Install an application, will assert administrative rights +## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateypackage +## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyinstallpackage +## add additional optional arguments as necessary +##Install-ChocolateyPackage $packageName $fileType $silentArgs $url [$url64 -validExitCodes $validExitCodes -checksum $checksum -checksumType $checksumType -checksum64 $checksum64 -checksumType64 $checksumType64] + +## Download and unpack a zip file - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyzippackage +##Install-ChocolateyZipPackage $packageName $url $toolsDir [$url64 -checksum $checksum -checksumType $checksumType -checksum64 $checksum64 -checksumType64 $checksumType64] + +## Install Visual Studio Package - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyvsixpackage +#Install-ChocolateyVsixPackage $packageName $url [$vsVersion] [-checksum $checksum -checksumType $checksumType] +#Install-ChocolateyVsixPackage @packageArgs + +## see the full list at https://docs.chocolatey.org/en-us/create/functions + +## downloader that the main helpers use to download items +## if removing $url64, please remove from here +## - https://docs.chocolatey.org/en-us/create/functions/get-chocolateywebfile +#Get-ChocolateyWebFile $packageName 'DOWNLOAD_TO_FILE_FULL_PATH' $url $url64 + +## Installer, will assert administrative rights - used by Install-ChocolateyPackage +## use this for embedding installers in the package when not going to community feed or when you have distribution rights +## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyinstallpackage +#Install-ChocolateyInstallPackage $packageName $fileType $silentArgs '_FULLFILEPATH_' -validExitCodes $validExitCodes + +## Unzips a file to the specified location - auto overwrites existing content +## - https://docs.chocolatey.org/en-us/create/functions/get-chocolateyunzip +#Get-ChocolateyUnzip "FULL_LOCATION_TO_ZIP.zip" $toolsDir + +## Runs processes asserting UAC, will assert administrative rights - used by Install-ChocolateyInstallPackage +## - https://docs.chocolatey.org/en-us/create/functions/start-chocolateyprocessasadmin +#Start-ChocolateyProcessAsAdmin 'STATEMENTS_TO_RUN' 'Optional_Application_If_Not_PowerShell' -validExitCodes $validExitCodes + +## To avoid quoting issues, you can also assemble your -Statements in another variable and pass it in +#$appPath = "$env:ProgramFiles\appname" +##Will resolve to C:\Program Files\appname +#$statementsToRun = "/C `"$appPath\bin\installservice.bat`"" +#Start-ChocolateyProcessAsAdmin $statementsToRun cmd -validExitCodes $validExitCodes + +## add specific folders to the path - any executables found in the chocolatey package +## folder will already be on the path. This is used in addition to that or for cases +## when a native installer doesn't add things to the path. +## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateypath +#Install-ChocolateyPath 'LOCATION_TO_ADD_TO_PATH' 'User_OR_Machine' # Machine will assert administrative rights + +## Add specific files as shortcuts to the desktop +## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyshortcut +#$target = Join-Path $toolsDir "$($packageName).exe" +# Install-ChocolateyShortcut -shortcutFilePath "" -targetPath "" [-workingDirectory "C:\" -arguments "C:\test.txt" -iconLocation "C:\test.ico" -description "This is the description"] + +## Outputs the bitness of the OS (either "32" or "64") +## - https://docs.chocolatey.org/en-us/create/functions/get-osarchitecturewidth +#$osBitness = Get-ProcessorBits + +## Set persistent Environment variables +## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyenvironmentvariable +#Install-ChocolateyEnvironmentVariable -variableName "SOMEVAR" -variableValue "value" [-variableType = 'Machine' #Defaults to 'User'] + +## Set up a file association +## - https://docs.chocolatey.org/en-us/create/functions/install-chocolateyfileassociation +#Install-ChocolateyFileAssociation + +## Adding a shim when not automatically found - Chocolatey automatically shims exe files found in package directory. +## - https://docs.chocolatey.org/en-us/create/functions/install-binfile +## - https://docs.chocolatey.org/en-us/create/create-packages#how-do-i-exclude-executables-from-getting-shims +#Install-BinFile + +##PORTABLE EXAMPLE +#$toolsDir = "$(Split-Path -parent $MyInvocation.MyCommand.Definition)" +# despite the name "Install-ChocolateyZipPackage" this also works with 7z archives +#Install-ChocolateyZipPackage $packageName $url $toolsDir $url64 +## END PORTABLE EXAMPLE diff --git a/packaging/choco/meta/tools/chocolateyuninstall.ps1 b/packaging/choco/meta/tools/chocolateyuninstall.ps1 new file mode 100644 index 0000000..3d7af68 --- /dev/null +++ b/packaging/choco/meta/tools/chocolateyuninstall.ps1 @@ -0,0 +1,78 @@ +# IMPORTANT: Before releasing this package, copy/paste the next 2 lines into PowerShell to remove all comments from this file: +# $f='c:\path\to\thisFile.ps1' +# gc $f | ? {$_ -notmatch "^\s*#"} | % {$_ -replace '(^.*?)\s*?[^``]#.*','$1'} | Out-File $f+".~" -en utf8; mv -fo $f+".~" $f + +## NOTE: In 80-90% of the cases (95% with licensed versions due to Package Synchronizer and other enhancements), +## AutoUninstaller should be able to detect and handle registry uninstalls without a chocolateyUninstall.ps1. +## See https://docs.chocolatey.org/en-us/choco/commands/uninstall +## and https://docs.chocolatey.org/en-us/create/functions/uninstall-chocolateypackage + +## If this is an MSI, ensure 'softwareName' is appropriate, then clean up comments and you are done. +## If this is an exe, change fileType, silentArgs, and validExitCodes + +$ErrorActionPreference = 'Stop' # stop on all errors +$packageArgs = @{ + packageName = $env:ChocolateyPackageName + softwareName = 'vcf-generator-lite*' #part or all of the Display Name as you see it in Programs and Features. It should be enough to be unique + fileType = 'EXE_MSI_OR_MSU' #only one of these: MSI or EXE (ignore MSU for now) + # MSI + silentArgs = "/qn /norestart" + validExitCodes= @(0, 3010, 1605, 1614, 1641) # https://msdn.microsoft.com/en-us/library/aa376931(v=vs.85).aspx + # OTHERS + # Uncomment matching EXE type (sorted by most to least common) + #silentArgs = '/S' # NSIS + #silentArgs = '/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-' # Inno Setup + #silentArgs = '/s' # InstallShield + #silentArgs = '/s /v"/qn"' # InstallShield with MSI + #silentArgs = '/s' # Wise InstallMaster + #silentArgs = '-s' # Squirrel + #silentArgs = '-q' # Install4j + #silentArgs = '-s -u' # Ghost + # Note that some installers, in addition to the silentArgs above, may also need assistance of AHK to achieve silence. + #silentArgs = '' # none; make silent with input macro script like AutoHotKey (AHK) + # https://community.chocolatey.org/packages/autohotkey.portable + #validExitCodes= @(0) #please insert other valid exit codes here +} + +[array]$key = Get-UninstallRegistryKey -SoftwareName $packageArgs['softwareName'] + +if ($key.Count -eq 1) { + $key | % { + $packageArgs['file'] = "$($_.UninstallString)" #NOTE: You may need to split this if it contains spaces, see below + + if ($packageArgs['fileType'] -eq 'MSI') { + # The Product Code GUID is all that should be passed for MSI, and very + # FIRST, because it comes directly after /x, which is already set in the + # Uninstall-ChocolateyPackage msiargs (facepalm). + $packageArgs['silentArgs'] = "$($_.PSChildName) $($packageArgs['silentArgs'])" + + # Don't pass anything for file, it is ignored for msi (facepalm number 2) + # Alternatively if you need to pass a path to an msi, determine that and + # use it instead of the above in silentArgs, still very first + $packageArgs['file'] = '' + } else { + # NOTES: + # - You probably will need to sanitize $packageArgs['file'] as it comes from the registry and could be in a variety of fun but unusable formats + # - Split args from exe in $packageArgs['file'] and pass those args through $packageArgs['silentArgs'] or ignore them + # - Ensure you don't pass double quotes in $file (aka $packageArgs['file']) - otherwise you will get "Illegal characters in path when you attempt to run this" + # - Review the code for auto-uninstaller for all of the fun things it does in sanitizing - https://github.com/chocolatey/choco/blob/bfe351b7d10c798014efe4bfbb100b171db25099/src/chocolatey/infrastructure.app/services/AutomaticUninstallerService.cs#L142-L192 + } + + Uninstall-ChocolateyPackage @packageArgs + } +} elseif ($key.Count -eq 0) { + Write-Warning "$packageName has already been uninstalled by other means." +} elseif ($key.Count -gt 1) { + Write-Warning "$($key.Count) matches found!" + Write-Warning "To prevent accidental data loss, no programs will be uninstalled." + Write-Warning "Please alert package maintainer the following keys were matched:" + $key | % {Write-Warning "- $($_.DisplayName)"} +} + +## OTHER POWERSHELL FUNCTIONS +## https://docs.chocolatey.org/en-us/create/functions +#Uninstall-ChocolateyZipPackage $packageName # Only necessary if you did not unpack to package directory - see https://docs.chocolatey.org/en-us/create/functions/uninstall-chocolateyzippackage +#Uninstall-ChocolateyEnvironmentVariable - https://docs.chocolatey.org/en-us/create/functions/uninstall-chocolateyenvironmentvariable +#Uninstall-BinFile # Only needed if you used Install-BinFile - see https://docs.chocolatey.org/en-us/create/functions/uninstall-binfile +## Remove any shortcuts you added in the install script. + diff --git a/packaging/choco/meta/vcf-generator-lite.template.nuspec b/packaging/choco/meta/vcf-generator-lite.template.nuspec new file mode 100644 index 0000000..bba6bdb --- /dev/null +++ b/packaging/choco/meta/vcf-generator-lite.template.nuspec @@ -0,0 +1,36 @@ + + + + + + vcf-generator-lite + $dynamic:{version_windows} + $dynamic:{repository_url} + $dynamic:{author} + + + + $dynamic:{display_name} + $dynamic:{author} + $dynamic:{repository_url} + $dynamic:{icon_url_cdn} + $dynamic:{copyright} + $dynamic:{license_url} + true + $dynamic:{repository_url} + $dynamic:{bug_tracker_url} + vcf-generator make-vcf vcf vcard-generator make-vcard vcard contacts + $dynamic:{summary} + + $dynamic:{release_notes} + + + + + + + + + + \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index e2ccce7..4c462f1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -46,6 +46,7 @@ format = "ruff format" build-installer.script = "scripts.build_app:build_installer" build-portable.script = "scripts.build_app:build_portable" build-zipapp.script = "scripts.build_app:build_zipapp" +build-chocopkgs.script = "scripts.build_app:build_chocolatey_packages" build-wheel = "uv build --wheel" [tool.uv] diff --git a/scripts/build_app/__init__.py b/scripts/build_app/__init__.py index 39f1fde..ff38247 100644 --- a/scripts/build_app/__init__.py +++ b/scripts/build_app/__init__.py @@ -1,3 +1,4 @@ +from scripts.build_app.chocolatey import build_chocolatey_packages as build_chocolatey_packages from scripts.build_app.innosetup import build_installer as build_installer from scripts.build_app.portable import build_portable as build_portable from scripts.build_app.wheel import build_wheel as build_wheel diff --git a/scripts/build_app/chocolatey.py b/scripts/build_app/chocolatey.py new file mode 100644 index 0000000..bb0618d --- /dev/null +++ b/scripts/build_app/chocolatey.py @@ -0,0 +1,130 @@ +import hashlib +import shutil +import subprocess +from collections.abc import Mapping +from contextlib import contextmanager +from pathlib import Path +from string import Template + +from scripts.app_metadata import app_metadata, app_version_variants +from scripts.build_app.common import PATH_DIST, PATH_PACKAGING, require_external_tool +from scripts.build_app.innosetup import PATH_DIST_INSTALLER, ensure_installer_dist + +PATH_PACKAGING_CHOCOLATEY = PATH_PACKAGING.joinpath("choco") + + +class ChocolateyTemplate(Template): + delimiter = "$dynamic:" + + +def fill_template_file( + template_file: Path, + generated_nuspec: Path, + template_filler: Mapping[str, str], + *, + encoding: str = "utf-8", +): + with template_file.open(encoding=encoding) as f: + content = f.read() + + try: + formatted_content = ChocolateyTemplate(content).substitute(template_filler) + except KeyError as e: + raise KeyError(f"Missing template key in {template_file}") from e + except ValueError as e: + raise ValueError(f"Invalid template {template_file}") from e + + with generated_nuspec.open(mode="w", encoding=encoding) as f: + f.write(formatted_content) + + +def file_sha256(file_path: Path, block_size: int = 65536): + sha256_hash = hashlib.sha256() + with file_path.open("rb") as f: + for chunk in iter(lambda: f.read(block_size), b""): + sha256_hash.update(chunk) + return sha256_hash.hexdigest() + + +def get_chocolatey_template_filler() -> Mapping[str, str]: + with Path("LICENSE").open(encoding="utf8") as license_file: + license_content = license_file.read() + with Path("README_choco.md").open(encoding="utf8") as readme_file: + readme_content = readme_file.read() + + return { + "display_name": app_metadata.display_name, + "version_windows": app_version_variants.windows, + "version_wheel": app_version_variants.wheel, + "license_url": "https://gitee.com/hellotool/VCFGeneratorLiteWithTkinter/blob/master/LICENSE", + "license_content": license_content, + "repository_url": app_metadata.repository or "", + "bug_tracker_url": app_metadata.bug_tracker or "", + "release_url": app_metadata.bug_tracker or "", + "author": app_metadata.author or "", + "icon_url_cdn": "https://cdn.jsdelivr.net/gh/hellotool/VCFGeneratorLiteWithTkinter@f1f5dac3e98e9a375bc68951cade995d611bb094/assets/images/icon.svg", + "summary": app_metadata.summary or "", + "description": readme_content, + "release_notes": app_metadata.release_notes or "", + "copyright": app_metadata.copyright or "", + "installer_sha256": file_sha256(PATH_DIST_INSTALLER), + "installer_filename": PATH_DIST_INSTALLER.name, + } + + +def get_chocolatey_template_encoding(path: Path) -> str: + if path.name.endswith(".ps1") or path.name == "LICENSE.txt": + return "utf-8-sig" + return "utf-8" + + +@contextmanager +def prepare_chocolatey_package(template_filler: Mapping[str, str], package_dir: Path): + tools_template_dir = package_dir / "tools-template" + tools_generated_dir = package_dir / "tools-generated" + template_nuspec_path = next(package_dir.glob("*.template.nuspec")) + generated_nuspec_path = template_nuspec_path.with_stem( + template_nuspec_path.stem[: -len(".template")] + ".generated" + ) + + if tools_generated_dir.exists(): + shutil.rmtree(tools_generated_dir) + + if tools_template_dir.exists() and tools_template_dir.is_dir(): + shutil.copytree(tools_template_dir, tools_generated_dir) + + for file_path in tools_generated_dir.rglob("*"): + if not file_path.is_file(): + continue + + fill_template_file( + file_path, + file_path, + template_filler, + encoding=get_chocolatey_template_encoding(file_path), + ) + else: + tools_generated_dir.mkdir(exist_ok=True) + + if generated_nuspec_path.is_file(): + generated_nuspec_path.unlink() + + fill_template_file(template_nuspec_path, generated_nuspec_path, template_filler) + + yield generated_nuspec_path + + shutil.rmtree(tools_generated_dir) + generated_nuspec_path.unlink() + + +def pack_with_chocolatey(spec_path: Path): + choco_path = require_external_tool("choco", "Chocolatey") + subprocess.run([choco_path, "pack", spec_path, "--output-directory", PATH_DIST.absolute()], check=True) # noqa: S603 + + +def build_chocolatey_packages(): + ensure_installer_dist() + template_filler: Mapping[str, str] = get_chocolatey_template_filler() + for package_dir in PATH_PACKAGING_CHOCOLATEY.iterdir(): + with prepare_chocolatey_package(template_filler, package_dir) as nuspec_file_path: + pack_with_chocolatey(nuspec_file_path)