Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 77 additions & 11 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -307,6 +324,7 @@ jobs:
}

- name: Create NSIS Installer
if: ${{ inputs.create_installer }}
shell: pwsh
run: |
choco install nsis -y
Expand All @@ -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"
Expand Down Expand Up @@ -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
}
45 changes: 34 additions & 11 deletions scripts/export_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}
Expand All @@ -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
Expand Down
Loading