diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..17f5887 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,80 @@ +name: Build Avida-ED Onefile +on: + workflow_dispatch: + inputs: + ed_versions: + description: "Comma-separated versions (v3,v4)" + default: "v3,v4" + required: true + +jobs: + build: + strategy: + fail-fast: false + matrix: + os: [ubuntu-22.04, windows-2022, macos-13] + ver: [v3, v4] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + + - name: Set up Rust + uses: dtolnay/rust-toolchain@stable + + - name: Fetch assets via docker compose (using URL fallback on mac/win if docker unavailable) + shell: bash + run: | + if command -v docker >/dev/null 2>&1; then + docker compose run --rm fetch-${{ matrix.ver }} + else + MODE=url URL=https://avida-ed.msu.edu/app4/ OUTDIR=./apps/${{ matrix.ver }} bash tools/fetch_assets.sh + fi + + - name: Inject webroot + run: | + rm -rf server-ui/webroot + mkdir -p server-ui/webroot + rsync -a apps/${{ matrix.ver }}/Avida-ED-Eco/ server-ui/webroot/Avida-ED-Eco/ + + - name: Build binary + run: | + cd server-ui + cargo build --release + + - name: Package (Linux AppImage) + if: startsWith(matrix.os, 'ubuntu') + run: | + sudo apt-get update && sudo apt-get install -y imagemagick || true + bash packaging/linux/make_appimage.sh ${{ matrix.ver }} server-ui/target/release/avidaed_onefile + mv Avida-ED-${{ matrix.ver }}-Linux-x86_64.AppImage Avida-ED-${{ matrix.ver }}-Linux-x86_64.AppImage + + - name: Package (Windows EXE) + if: startsWith(matrix.os, 'windows') + shell: powershell + run: | + $WV2 = "$env:RUNNER_TEMP\WebView2Fixed" # pre-cached in your org, or downloaded here + # TODO: fetch/unzip WebView2 Fixed into $WV2 if not cached + powershell -ExecutionPolicy Bypass -File packaging/windows/make_windows_sfx.ps1 ` + -Version ${{ matrix.ver }} ` + -BinPath server-ui/target/release/avidaed_onefile.exe ` + -WV2Fixed $WV2 + Move-Item packaging\windows\Avida-ED-${{ matrix.ver }}-Windows.exe . + + - name: Package (macOS .app) + if: startsWith(matrix.os, 'macos') + run: | + bash packaging/mac/make_macos_bundle.sh ${{ matrix.ver }} server-ui/target/release/avidaed_onefile + # Optionally make a DMG + # brew install create-dmg + # create-dmg "Avida-ED-${{ matrix.ver }}.app" + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: Avida-ED-${{ matrix.ver }}-${{ matrix.os }} + path: | + *.AppImage + *.exe + Avida-ED-${{ matrix.ver }}.app + if-no-files-found: ignore + diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..75e66ed --- /dev/null +++ b/Makefile @@ -0,0 +1,44 @@ +VER ?= v4 # or v3 + +.PHONY: fetch-$(VER) inject-$(VER) build-linux build-mac build-win appimage winexe macapp all + +fetch-v3: + docker compose run --rm fetch-v3 +fetch-v4: + docker compose run --rm fetch-v4 + +inject-$(VER): + rm -rf server-ui/webroot + mkdir -p server-ui/webroot + rsync -a apps/$(VER)/Avida-ED-Eco/ server-ui/webroot/Avida-ED-Eco/ + +# Build binaries natively on each OS runner +build-linux: inject-$(VER) + cd server-ui && cargo build --release + +build-mac: inject-$(VER) + cd server-ui && cargo build --release + +build-win: inject-$(VER) + cd server-ui && cargo build --release + +# Package +appimage: build-linux + bash packaging/linux/make_appimage.sh $(VER) server-ui/target/release/avidaed_onefile + +winexe: build-win + # Example: pass location of WebView2 Fixed runtime and built exe + powershell -ExecutionPolicy Bypass -File packaging/windows/make_windows_sfx.ps1 \ + -Version $(VER) \ + -BinPath $(CURDIR)/server-ui/target/release/avidaed_onefile.exe \ + -WV2Fixed "C:\SDKs\WebView2.FixedRuntime" + +macapp: build-mac + bash packaging/mac/make_macos_bundle.sh $(VER) server-ui/target/release/avidaed_onefile + +all: + @echo "Targets:" + @echo " make fetch-v3 | fetch-v4" + @echo " make build-linux | build-mac | build-win" + @echo " make appimage | winexe | macapp" + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..75acba3 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,21 @@ +version: "3.8" +services: + fetch-v3: + build: { context: ./tools, dockerfile: Dockerfile.fetch } + environment: + MODE: "docker" # or "url" + ED_VER: "v3" + OUTDIR: "/out" + volumes: + - ./apps/v3:/out + # If MODE=url, also pass URL as env + + fetch-v4: + build: { context: ./tools, dockerfile: Dockerfile.fetch } + environment: + MODE: "docker" + ED_VER: "v4" + OUTDIR: "/out" + volumes: + - ./apps/v4:/out + diff --git a/packaging/linux/Apprun b/packaging/linux/Apprun new file mode 100644 index 0000000..e0202d7 --- /dev/null +++ b/packaging/linux/Apprun @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +HERE="$(dirname "$(readlink -f "$0")")" +exec "$HERE/usr/bin/avidaed_onefile" + diff --git a/packaging/linux/Dockerfile.appimage b/packaging/linux/Dockerfile.appimage new file mode 100644 index 0000000..cb83deb --- /dev/null +++ b/packaging/linux/Dockerfile.appimage @@ -0,0 +1,10 @@ +FROM ubuntu:22.04 +RUN apt-get update && apt-get install -y \ + build-essential curl git libgtk-3-dev libayatana-appindicator3-dev \ + libwebkit2gtk-4.0-dev pkg-config cmake patchelf desktop-file-utils \ + && rm -rf /var/lib/apt/lists/* +# appimagetool +RUN curl -L https://github.com/AppImage/AppImageKit/releases/download/13/appimagetool-x86_64.AppImage -o /usr/local/bin/appimagetool \ + && chmod +x /usr/local/bin/appimagetool +WORKDIR /work + diff --git a/packaging/linux/desktop/avidaed.desktop b/packaging/linux/desktop/avidaed.desktop new file mode 100644 index 0000000..1b120eb --- /dev/null +++ b/packaging/linux/desktop/avidaed.desktop @@ -0,0 +1,7 @@ +[Desktop Entry] +Name=Avida-ED +Exec=AppRun +Type=Application +Icon=avidaed +Categories=Education;Science; + diff --git a/packaging/linux/make_appimage.sh b/packaging/linux/make_appimage.sh new file mode 100644 index 0000000..a24c132 --- /dev/null +++ b/packaging/linux/make_appimage.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail +VER="${1:?version folder, e.g., v4}" +BINPATH="${2:-../..//server-ui/target/release/avidaed_onefile}" + +APPDIR="AvidaED-${VER}.AppDir" +rm -rf "$APPDIR"; mkdir -p "$APPDIR/usr/bin" "$APPDIR/usr/share/icons/hicolor/256x256/apps" +cp "$BINPATH" "$APPDIR/usr/bin/avidaed_onefile" +cp "$(dirname "$0")/AppRun" "$APPDIR/AppRun" +chmod +x "$APPDIR/AppRun" +cp "$(dirname "$0")/desktop/avidaed.desktop" "$APPDIR/avidaed.desktop" + +# placeholder icon; replace with your PNG +convert -size 256x256 xc:white "$APPDIR/usr/share/icons/hicolor/256x256/apps/avidaed.png" 2>/dev/null || true + +appimagetool "$APPDIR" "Avida-ED-${VER}-Linux-x86_64.AppImage" +echo "Wrote Avida-ED-${VER}-Linux-x86_64.AppImage" + diff --git a/packaging/mac/info.plist.tmpl b/packaging/mac/info.plist.tmpl new file mode 100644 index 0000000..31b8070 --- /dev/null +++ b/packaging/mac/info.plist.tmpl @@ -0,0 +1,15 @@ + + + + + CFBundleNameAvida-ED + CFBundleDisplayNameAvida-ED + CFBundleIdentifierorg.avidaed.{{VER}} + CFBundleVersion1.0 + CFBundleShortVersionString1.0 + CFBundleExecutableAvida-ED + LSMinimumSystemVersion10.13 + NSHighResolutionCapable + + + diff --git a/packaging/mac/make_macos_bundle.sh b/packaging/mac/make_macos_bundle.sh new file mode 100644 index 0000000..0cabe13 --- /dev/null +++ b/packaging/mac/make_macos_bundle.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail +VER="${1:?v3|v4}" +BINPATH="${2:-../../server-ui/target/release/avidaed_onefile}" +APP="Avida-ED-${VER}.app" +rm -rf "$APP" +mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" +cp "$BINPATH" "$APP/Contents/MacOS/Avida-ED" +chmod +x "$APP/Contents/MacOS/Avida-ED" +# Info.plist +sed "s/{{VER}}/${VER}/g" "$(dirname "$0")/Info.plist.tmpl" > "$APP/Contents/Info.plist" +# Icon placeholder +sips -s format icns /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns --out "$APP/Contents/Resources/AppIcon.icns" >/dev/null 2>&1 || true +echo "Wrote $APP (unsigned)." + diff --git a/packaging/windows/_make_windows_sfx.ps1 b/packaging/windows/_make_windows_sfx.ps1 new file mode 100644 index 0000000..247c1ba --- /dev/null +++ b/packaging/windows/_make_windows_sfx.ps1 @@ -0,0 +1,32 @@ +param( + [Parameter(Mandatory=$true)][string]$Version, # v3 or v4 + [Parameter(Mandatory=$true)][string]$BinPath, # path to target\release\avidaed_onefile.exe + [Parameter(Mandatory=$true)][string]$WV2Fixed # path to WebView2 Fixed Runtime folder +) +$Work = Join-Path $PSScriptRoot "payload_$Version" +Remove-Item $Work -Recurse -Force -ErrorAction SilentlyContinue +New-Item -ItemType Directory -Force -Path $Work | Out-Null +Copy-Item $BinPath -Destination (Join-Path $Work "avidaed_onefile.exe") +Copy-Item $WV2Fixed -Destination (Join-Path $Work "WebView2Fixed") -Recurse + +# launcher +$runbat = @" +@echo off +setlocal +set WEBVIEW2_BROWSER_EXECUTABLE_FOLDER=%~dp0WebView2Fixed +start "" "%~dp0avidaed_onefile.exe" +"@ +$runbat | Out-File -Encoding ASCII (Join-Path $Work "run.bat") + +# create 7z archive +$SevenZip = "C:\Program Files\7-Zip\7z.exe" +& $SevenZip a -t7z (Join-Path $PSScriptRoot "payload_$Version.7z") "$Work\*" + +# concatenate SFX + config + archive -> one EXE +$Sfx = Join-Path $PSScriptRoot "7z.sfx" +$Cfg = Join-Path $PSScriptRoot "config.txt" +$Out = Join-Path $PSScriptRoot ("Avida-ED-$Version-Windows.exe") +Get-Content $Sfx -Encoding Byte, $Cfg -Encoding Byte, (Join-Path $PSScriptRoot "payload_$Version.7z") -Encoding Byte | + Set-Content $Out -Encoding Byte +Write-Host "Wrote $Out" + diff --git a/packaging/windows/config.txt b/packaging/windows/config.txt new file mode 100644 index 0000000..4790f92 --- /dev/null +++ b/packaging/windows/config.txt @@ -0,0 +1,9 @@ +;!@Install@!UTF-8! +Title="Avida-ED" +BeginPrompt="Install and run Avida-ED?" +RunProgram="run.bat" +; extract to temp folder +ExtractTitle="Avida-ED" +GUIMode="2" +;!@InstallEnd@! + diff --git a/server-ui/Cargo.toml b/server-ui/Cargo.toml new file mode 100644 index 0000000..ed47efd --- /dev/null +++ b/server-ui/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "avidaed_onefile" +version = "0.1.0" +edition = "2021" + +[dependencies] +wry = "0.39" # WebView wrapper: WebView2/WKWebView/WebKitGTK +tiny-http = "0.12" # tiny static server +include_dir = "0.7" # embed webroot into binary +mime_guess = "2.0" +once_cell = "1.19" +anyhow = "1.0" + +[profile.release] +lto = true +codegen-units = 1 +strip = "symbols" + diff --git a/server-ui/build.rs b/server-ui/build.rs new file mode 100644 index 0000000..caa586f --- /dev/null +++ b/server-ui/build.rs @@ -0,0 +1,3 @@ +fn main() { + println!("cargo:rerun-if-changed=webroot"); +} diff --git a/server-ui/src/main.rs b/server-ui/src/main.rs new file mode 100644 index 0000000..1d0dbe5 --- /dev/null +++ b/server-ui/src/main.rs @@ -0,0 +1,63 @@ +use include_dir::{include_dir, Dir}; +use tiny_http::{Request, Response, Method}; +use wry::{ + application::event::{Event, StartCause, WindowEvent}, + application::event_loop::{ControlFlow, EventLoop}, + application::window::WindowBuilder, + webview::WebViewBuilder +}; +use std::{net::TcpListener, thread, time::Duration}; +use anyhow::Result; + +static WEBROOT: Dir = include_dir!("$CARGO_MANIFEST_DIR/webroot"); +static DEFAULT_PATH: &str = "/Avida-ED-Eco/index.html"; + +fn mime(p: &str)->&'static str{ + if p.ends_with(".wasm"){return "application/wasm";} + if p.ends_with(".js"){return "application/javascript";} + if p.ends_with(".css"){return "text/css";} + if p.ends_with(".html") || p.ends_with(".htm"){return "text/html; charset=utf-8";} + mime_guess::from_path(p).first_raw().unwrap_or("application/octet-stream") +} +fn serve(mut req:Request){ + if req.method()!=&Method::Get && req.method()!=&Method::Head{ + let _=req.respond(Response::from_string("Method Not Allowed").with_status_code(405));return; + } + let mut path=req.url().to_string(); + if let Some(i)=path.find('?'){path.truncate(i);} + if path=="/"{path=DEFAULT_PATH.to_string();} + let fpath=path.trim_start_matches('/'); + if let Some(f)=WEBROOT.get_file(fpath){ + let mut resp=Response::from_data(f.contents().to_vec()); + let _=resp.add_header(tiny_http::Header::from_bytes(b"Content-Type", mime(&path)).unwrap()); + let _=resp.add_header(tiny_http::Header::from_bytes(b"Cache-Control", b"no-store").unwrap()); + let _=req.respond(resp); + }else{ + let _=req.respond(Response::from_string("Not Found").with_status_code(404)); + } +} +fn main()->Result<()>{ + let listener=TcpListener::bind(("127.0.0.1",0))?; listener.set_nonblocking(true)?; + let port=listener.local_addr()?.port(); let srv=tiny_http::Server::from_tcp(listener)?; + std::thread::spawn(move||{ + loop{ + match srv.recv_timeout(Duration::from_millis(200)){ + Ok(Some(r))=>serve(r), Ok(None)=>continue, Err(_)=>break + } + } + }); + let url=format!("http://127.0.0.1:{port}{DEFAULT_PATH}"); + let event_loop=EventLoop::new()?; + let window=WindowBuilder::new() + .with_title("Avida-ED") + .with_inner_size(wry::application::dpi::LogicalSize::new(1280.0,800.0)) + .build(&event_loop)?; + let _wv=WebViewBuilder::new(&window)?.with_url(&url)?.build()?; + event_loop.run(move|e,_,cf|{ + *cf=wry::application::event_loop::ControlFlow::Wait; + if let Event::WindowEvent{event:WindowEvent::CloseRequested,..}=e{*cf=wry::application::event_loop::ControlFlow::Exit;} + if let Event::NewEvents(StartCause::Init)=e{} + })?; + Ok(()) +} + diff --git a/tools/Dockerfile.fetch b/tools/Dockerfile.fetch new file mode 100644 index 0000000..4b505cb --- /dev/null +++ b/tools/Dockerfile.fetch @@ -0,0 +1,5 @@ +FROM alpine:3.20 +RUN apk add --no-cache ca-certificates curl bash rsync +WORKDIR /work +COPY fetch_assets.sh /work/fetch_assets.sh +ENTRYPOINT ["/work/fetch_assets.sh"] diff --git a/tools/fetch_assets.sh b/tools/fetch_assets.sh new file mode 100644 index 0000000..89e71fe --- /dev/null +++ b/tools/fetch_assets.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail +MODE="${MODE:-docker}" # docker|url +ED_VER="${ED_VER:-v4}" # v3|v4 +OUTDIR="${OUTDIR:-/out}" + +mkdir -p "$OUTDIR" +case "$MODE" in + docker) + # 1) Start your aed-docker container and copy its served webroot + # Expectation: container serves Avida-ED-Eco at /usr/share/nginx/html/Avida-ED-Eco + CID="$(docker create --name aedtmp_$ED_VER welsberr/aed-docker:$ED_VER)" + trap 'docker rm -f aedtmp_'"$ED_VER"' >/dev/null 2>&1 || true' EXIT + docker cp "aedtmp_$ED_VER:/usr/share/nginx/html/." "$OUTDIR/" + ;; + url) + URL="${URL:-https://avida-ed.msu.edu/app4/}" + apk add --no-cache wget >/dev/null 2>&1 || true + wget --recursive --no-parent --page-requisites --adjust-extension \ + --compression=auto --convert-links --timestamping \ + --directory-prefix "$OUTDIR" \ + "$URL" + # normalize into $OUTDIR/Avida-ED-Eco as needed + if [ ! -d "$OUTDIR/Avida-ED-Eco" ]; then + SUB="$(find "$OUTDIR" -type f -name index.html | head -n1)" + [ -n "$SUB" ] && rsync -a "$(dirname "$SUB")"/ "$OUTDIR/Avida-ED-Eco"/ + fi + ;; + *) echo "Unknown MODE=$MODE" >&2; exit 1;; +esac + +echo "Assets fetched to $OUTDIR" diff --git a/tools/inject_webroot.rs b/tools/inject_webroot.rs new file mode 100644 index 0000000..1e867ca --- /dev/null +++ b/tools/inject_webroot.rs @@ -0,0 +1,10 @@ +use std::{env, fs, path::Path}; +fn main() { + let args:Vec=env::args().collect(); + if args.len()!=3{eprintln!("usage: inject_webroot "); std::process::exit(2);} + let (src,dst)=(&args[1],&args[2]); + if Path::new(dst).exists(){fs::remove_dir_all(dst).ok();} + fs::create_dir_all(dst).unwrap(); + fs_extra::dir::copy(src,dst,&fs_extra::dir::CopyOptions{overwrite:true,copy_inside:true, ..Default::default()}).unwrap(); +} +