diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20ab784..78d8df8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,6 +2,22 @@ name: Build SmartEye on: workflow_dispatch: + inputs: + create_installer: + description: "Build an NSIS installer in addition to the portable app zip" + required: true + type: boolean + default: true + create_release: + description: "Create or update a GitHub Release with the production artifacts" + required: true + type: boolean + default: false + compress_with_upx: + description: "Compress SmartEye.exe with UPX before packaging" + required: true + type: boolean + default: false pull_request: push: branches: @@ -294,6 +310,7 @@ jobs: assume-yes-for-downloads: true - name: Compress executable with UPX + if: ${{ inputs.compress_with_upx }} shell: pwsh run: | choco install upx -y @@ -307,6 +324,7 @@ jobs: } - name: Create NSIS Installer + if: ${{ inputs.create_installer }} shell: pwsh run: | choco install nsis -y @@ -328,6 +346,7 @@ jobs: SetCompressorDictSize 64 Name "`${APPNAME} Setup" + RequestExecutionLevel admin !define MUI_ICON "frontend\assets\icons\icon.ico" !define MUI_UNICON "frontend\assets\icons\icon.ico" @@ -387,41 +406,88 @@ jobs: Write-Host "Installer created successfully!" - - name: Collect release files - id: release_files + - name: Collect production artifacts + id: production_artifacts shell: pwsh run: | $appInfo = Get-Content app_info.json | ConvertFrom-Json $appName = $appInfo.name $appVersion = $appInfo.version $appBuild = Get-Date -Format "yyyyMMdd" + $artifactDir = Join-Path $PWD "build\artifacts" + New-Item -Path $artifactDir -ItemType Directory -Force | Out-Null + + $distDir = Get-ChildItem -Path (Join-Path $PWD 'build') -Directory -Filter *.dist | Select-Object -First 1 + if (-not $distDir) { + throw "No .dist folder found" + } + + $portableStageRoot = Join-Path $env:RUNNER_TEMP "smarteye-portable" + $portableAppRoot = Join-Path $portableStageRoot "SmartEye" + Remove-Item -Path $portableStageRoot -Recurse -Force -ErrorAction SilentlyContinue + New-Item -Path $portableAppRoot -ItemType Directory -Force | Out-Null + Copy-Item -Path (Join-Path $distDir.FullName '*') -Destination $portableAppRoot -Recurse -Force + + @" + SmartEye portable production build + + Run SmartEye.exe from this folder. + Keep the files and folders together; the executable depends on the bundled runtime files. + If Windows SmartScreen appears, choose More info, then Run anyway for your own test build. + "@ | Out-File -FilePath (Join-Path $portableAppRoot "README_FIRST.txt") -Encoding UTF8 + + $portablePath = Join-Path $artifactDir "$appName-$appVersion-$appBuild-windows-portable.zip" + Compress-Archive -Path $portableAppRoot -DestinationPath $portablePath -Force $installerPath = Join-Path $PWD "build\SmartEyeInstaller.exe" - if (-not (Test-Path $installerPath)) { - throw "Installer not found at $installerPath" + $packagedInstallerPath = "" + if (Test-Path $installerPath) { + $packagedInstallerPath = Join-Path $artifactDir "$appName-$appVersion-$appBuild-windows-installer.exe" + Copy-Item -LiteralPath $installerPath -Destination $packagedInstallerPath -Force + } else { + Write-Host "Installer not found; portable zip will still be uploaded." } "app_name=$appName" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 "app_version=$appVersion" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 "app_build=$appBuild" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 - "installer_path=$installerPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + "portable_path=$portablePath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + "installer_path=$packagedInstallerPath" | Out-File -FilePath $env:GITHUB_OUTPUT -Append -Encoding utf8 + + Write-Host "Production artifacts:" + Get-ChildItem -Path $artifactDir -File | ForEach-Object { + Write-Host " - $($_.FullName) ($($_.Length) bytes)" + } + + - name: Upload production artifacts + uses: actions/upload-artifact@v4 + with: + name: SmartEye-windows-production-${{ steps.production_artifacts.outputs.app_version }}-${{ steps.production_artifacts.outputs.app_build }} + path: build/artifacts/* + if-no-files-found: error + retention-days: 14 - name: Create or update release + if: ${{ inputs.create_release }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: pwsh run: | - $appName = "${{ steps.release_files.outputs.app_name }}" - $appVersion = "${{ steps.release_files.outputs.app_version }}" - $appBuild = "${{ steps.release_files.outputs.app_build }}" + $appName = "${{ steps.production_artifacts.outputs.app_name }}" + $appVersion = "${{ steps.production_artifacts.outputs.app_version }}" + $appBuild = "${{ steps.production_artifacts.outputs.app_build }}" $tag = "v$appVersion" $name = "$appName v$appVersion (build $appBuild)" $repo = "${{ github.repository }}" - $installer = "${{ steps.release_files.outputs.installer_path }}" + $assets = @("${{ steps.production_artifacts.outputs.portable_path }}") + $installer = "${{ steps.production_artifacts.outputs.installer_path }}" + if ($installer -and (Test-Path $installer)) { + $assets += $installer + } gh release view $tag --repo $repo *> $null if ($LASTEXITCODE -ne 0) { - gh release create $tag $installer --repo $repo --title $name --generate-notes + gh release create $tag $assets --repo $repo --title $name --generate-notes } else { - gh release upload $tag $installer --repo $repo --clobber + gh release upload $tag $assets --repo $repo --clobber } diff --git a/scripts/export_report.py b/scripts/export_report.py index 740ab2b..440bbc4 100644 --- a/scripts/export_report.py +++ b/scripts/export_report.py @@ -791,10 +791,24 @@ def add_image_placeholder(document: Document, img_node: Tag, base_dir: Path) -> add_paragraph(document, f"[Image placeholder: {alt} - {src}]") -def add_centered_picture(document: Document, image_path: Path, width: float) -> None: +def _image_dimensions(image_path: Path) -> tuple[int, int] | None: + try: + from PIL import Image + + with Image.open(image_path) as image: + return image.size + except Exception: + return None + + +def add_centered_picture(document: Document, image_path: Path, width: float, max_height: float | None = None) -> None: paragraph = document.add_paragraph() paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER - paragraph.add_run().add_picture(str(image_path), width=Inches(width)) + shape = paragraph.add_run().add_picture(str(image_path), width=Inches(width)) + if max_height is not None and shape.height > Inches(max_height): + ratio = Inches(max_height) / shape.height + shape.width = int(shape.width * ratio) + shape.height = Inches(max_height) def add_caption(document: Document, text: str) -> None: @@ -844,7 +858,12 @@ def add_math_to_docx(document: Document, node: Tag, math_dir: Path | None) -> No png_path = math_dir / f"equation-{idx + 1:02d}.png" if math_dir and idx >= 0 else None if png_path and png_path.exists(): try: - add_centered_picture(document, png_path, width=4.2) + dimensions = _image_dimensions(png_path) + width = 4.2 + if dimensions: + pixel_width, _pixel_height = dimensions + width = min(5.8, max(1.2, pixel_width / 170.0)) + add_centered_picture(document, png_path, width=width, max_height=1.2) return except Exception: pass @@ -1025,6 +1044,10 @@ def render_visual_assets(html_path: Path, mermaid_dir: Path, math_dir: Path) -> mermaid_dir.mkdir(parents=True, exist_ok=True) math_dir.mkdir(parents=True, exist_ok=True) + for old_asset in mermaid_dir.glob("mermaid-*.png"): + old_asset.unlink(missing_ok=True) + for old_asset in math_dir.glob("equation-*.png"): + old_asset.unlink(missing_ok=True) browser_path = find_browser_executable() with sync_playwright() as playwright: launch_kwargs = {"headless": True} @@ -1044,17 +1067,17 @@ def render_visual_assets(html_path: Path, mermaid_dir: Path, math_dir: Path) -> continue figure.screenshot(path=str(mermaid_dir / f"mermaid-{idx + 1:02d}.png")) rendered_diagrams += 1 - equations = page.locator(".math.display svg") + equations = page.locator(".math.display") equation_count = equations.count() - if equation_count == 0: - equations = page.locator(".math.display mjx-container") - equation_count = equations.count() - if equation_count == 0: - equations = page.locator(".math.display") - equation_count = equations.count() rendered_equations = 0 for idx in range(equation_count): - equation = equations.nth(idx) + block = equations.nth(idx) + svg = block.locator("mjx-container > svg").nth(0) + if svg.count(): + equation = svg + else: + container = block.locator("mjx-container").nth(0) + equation = container if container.count() else block box = equation.bounding_box() if not box or box["width"] <= 0 or box["height"] <= 0: continue