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
11 changes: 10 additions & 1 deletion .github/workflows/build-wasm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,18 @@ jobs:
with:
python-version: '3.12'
- name: Test dependency download integrity and cache recovery
run: python scripts/wasm/test-downloads.py
run: |
python scripts/wasm/test-downloads.py
python scripts/wasm/test-vienna-release.py
- name: Test input, worker lifecycle and CSV handling
run: npm test --prefix webinterface
- name: Resolve newest stable ViennaRNA 2.x release and checksum
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
python scripts/wasm/vienna-release.py --latest > "$RUNNER_TEMP/vienna-release.json"
cat "$RUNNER_TEMP/vienna-release.json"
echo "WASM_VIENNA_RELEASE=$RUNNER_TEMP/vienna-release.json" >> "$GITHUB_ENV"
- name: Cross-compile dependencies and IntaRNA
run: bash scripts/wasm/build.sh
- name: Check real WASM predictions against the native reference
Expand Down
25 changes: 25 additions & 0 deletions ChangeLog
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@

## Technical changes and Optimizations

- build WASM with ViennaRNA 2.7.2 locally and the newest stable 2.x in CI;
verify published archive checksums, isolate versioned caches and retain
selected dependency provenance and notices (PR #292 review)

- BUGFIX : restore WASM dependency downloads using the pinned official ViennaRNA
release asset; verify downloads before caching and recover invalid caches
(PR #291)
Expand Down Expand Up @@ -249,6 +253,27 @@ energies, restricted partition sums, and trackers.
################################################################################
################################################################################

261022 Alexander Mitrofanov
* scripts/wasm/{vienna-release.py,vienna-release.json}, build.sh
+ Resolve the newest stable ViennaRNA >=2.7.2 and <3 from all official GitHub
release pages in CI; require its release tarball and published SHA-256.
* Pin 2.7.2 for reproducible local builds and accept saved release manifests.
Isolate RNAlib sources/installations and IntaRNA objects by archive identity
and keep Boost/zlib in a separate prefix to prevent reuse or include-path
shadowing of libraries/headers from another release (PR #292).
* scripts/wasm/patch-vienna.py
- Remove the dlib 19.24 compatibility patch; ViennaRNA 2.7.2 bundles dlib 20.0.
* scripts/wasm/{write-build-info.mjs,stage.sh,smoke-protocol.mjs,test-browser.py}
* Record the selected archive URL/checksum, check the compiled ViennaRNA
version, stage matching dependency notices and test actual version exports.
* scripts/wasm/test-vienna-release.py, .github/workflows/build-wasm.yml
+ Cover numeric version selection, pagination, major-3/prerelease exclusion,
missing/invalid asset metadata, API failures, manifest validation and
rejection of compiled engines with a stale ViennaRNA version.
* webinterface/docs/step1-wasm-compilation.md
* Document latest-2.x CI selection, pinned local reproduction, versioned
caches, current bundled dlib and archive provenance.

261022 Alexander Mitrofanov
* scripts/wasm/{build,download}.sh
* Use the official GitHub ViennaRNA 2.7.0 release asset and pin its SHA-256;
Expand Down
57 changes: 35 additions & 22 deletions scripts/wasm/build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@ repo=$(cd "$(dirname "$0")/../.." && pwd)
build=${WASM_BUILD_DIR:-"$repo/build-wasm"}
mkdir -p "$build"
build=$(cd "$build" && pwd)
prefix="$build/deps"
# Keep legacy RNAlib headers out of Boost's -I path: ordinary -I searches take
# precedence over the -isystem ViennaRNA paths produced by IntaRNA's configure.
prefix="$build/deps/common"
jobs=${JOBS:-2}
for tool in emcc em++ emar emranlib emconfigure emmake curl pkg-config node; do
for tool in emcc em++ emar emranlib emconfigure emmake curl pkg-config node python3; do
command -v "$tool" >/dev/null || { echo "Missing tool: $tool" >&2; exit 1; }
done
mkdir -p "$build/downloads" "$build/sources" "$prefix" "$build/site/wasm"
Expand All @@ -18,16 +20,27 @@ fetch https://zlib.net/fossils/zlib-1.3.1.tar.gz zlib-1.3.1.tar.gz \
9a93b2b7dfdac77ceba5a558a580e74667dd6fede4585b91eefb60f03b72df23
fetch https://archives.boost.io/release/1.87.0/source/boost_1_87_0.tar.bz2 boost_1_87_0.tar.bz2 \
af57be25cb4c4f4b413ed692fe378affb4352ea50fbe294a11ef548f4d527d89
fetch https://github.com/ViennaRNA/ViennaRNA/releases/download/v2.7.0/ViennaRNA-2.7.0.tar.gz ViennaRNA-2.7.0.tar.gz \
e2addf33e7c0dd4fff31d8e9a46fda8e9efcde0d199f789f20eb0a13b7f7135d
# CI supplies the newest stable 2.x manifest; local builds use the checked-in pin.
read -r vienna_version vienna_url vienna_sha256 < <(
python3 "$repo/scripts/wasm/vienna-release.py" --fields \
--manifest "${WASM_VIENNA_RELEASE:-$repo/scripts/wasm/vienna-release.json}"
)
echo "Building ViennaRNA $vienna_version (SHA-256 $vienna_sha256)"
fetch "$vienna_url" "ViennaRNA-$vienna_version.tar.gz" "$vienna_sha256"
# Separate source, installed RNAlib and IntaRNA objects by archive identity so
# changing the release cannot reuse headers, libraries or objects from an old one.
vienna_id="vienna-$vienna_version-$vienna_sha256"
vienna_prefix="$build/deps/$vienna_id"
vienna_sources="$build/sources/$vienna_id"
intarna_build="$build/intarna-$vienna_id"

# Keep pkg-config searches inside the WASM prefix, including configure probes.
export PKG_CONFIG_LIBDIR="$prefix/lib/pkgconfig"
export PKG_CONFIG_LIBDIR="$vienna_prefix/lib/pkgconfig:$prefix/lib/pkgconfig"
export PKG_CONFIG_PATH="$PKG_CONFIG_LIBDIR"
export CFLAGS='-O3 -fexceptions'
export CXXFLAGS='-O3 -fexceptions'
export CPPFLAGS="-I$prefix/include"
export LDFLAGS="-L$prefix/lib -fexceptions"
export CPPFLAGS="-I$vienna_prefix/include -I$prefix/include"
export LDFLAGS="-L$vienna_prefix/lib -L$prefix/lib -fexceptions"
if [[ ! -f "$prefix/lib/libz.a" ]]; then
tar -xzf "$build/downloads/zlib-1.3.1.tar.gz" -C "$build/sources"
(cd "$build/sources/zlib-1.3.1"
Expand All @@ -47,27 +60,26 @@ if [[ ! -f "$prefix/.boost-1.87.0-js-exceptions-complete" ]]; then
-sZLIB_LIBPATH="$prefix/lib" --prefix="$prefix" install)
touch "$prefix/.boost-1.87.0-js-exceptions-complete"
fi
if [[ ! -f "$prefix/lib/libRNA.a" ]]; then
tar -xzf "$build/downloads/ViennaRNA-2.7.0.tar.gz" -C "$build/sources"
(cd "$build/sources/ViennaRNA-2.7.0"
CPPFLAGS="$CPPFLAGS -I$PWD/src" emconfigure ./configure --host=wasm32-unknown-emscripten --prefix="$prefix" \
if [[ ! -f "$vienna_prefix/.complete" ]]; then
mkdir -p "$vienna_sources"
tar -xzf "$build/downloads/ViennaRNA-$vienna_version.tar.gz" -C "$vienna_sources"
(cd "$vienna_sources/ViennaRNA-$vienna_version"
CPPFLAGS="$CPPFLAGS -I$PWD/src" emconfigure ./configure --host=wasm32-unknown-emscripten --prefix="$vienna_prefix" \
--disable-shared --enable-static --disable-openmp --disable-pthreads \
--disable-lto --disable-simd --disable-vectorize --disable-mpfr \
--without-svm --without-gsl --without-swig --without-perl --without-python \
--without-doc --without-cla --without-check --without-kinfold \
--without-forester --without-rnalocmin --without-rnaxplorer
# Backport dlib's upstream char32_t typedef for modern libc++ char_traits.
# Only the downloaded dependency is patched; RNAlib algorithms are unchanged.
python3 "$repo/scripts/wasm/patch-vienna.py" "$PWD"
emmake make -C src/ViennaRNA -j "$jobs"
emmake make -C src/ViennaRNA install
emmake make install-pkgconfigDATA)
touch "$vienna_prefix/.complete"
fi

# Legacy Autoconf probes declare exit()/inflateEnd() with the wrong signatures,
# which wasm-ld rejects. Verify real dependency APIs before caching those probes.
em++ -std=c++23 -O2 -fexceptions -I"$prefix/include" \
"$repo/scripts/wasm/dependency-probe.cpp" -L"$prefix/lib" \
em++ -std=c++23 -O2 -fexceptions -I"$vienna_prefix/include" -I"$prefix/include" \
"$repo/scripts/wasm/dependency-probe.cpp" -L"$vienna_prefix/lib" -L"$prefix/lib" \
-lboost_filesystem -lboost_system -lboost_program_options -lboost_regex \
-lboost_iostreams -lboost_serialization -lRNA -lz -o "$build/dependency-probe.js"
node "$build/dependency-probe.js"
Expand All @@ -78,15 +90,15 @@ export ac_cv_lib_boost_regex_exit=yes
export ac_cv_lib_boost_iostreams_exit=yes
export ac_cv_lib_z_inflateEnd=yes
# RNAlib's .pc embeds a native-only -Wl,-fno-lto; this is a static WASM build.
export VRNA_LIBS="-L$prefix/lib -lRNA -lm"
export VRNA_LIBS="-L$vienna_prefix/lib -lRNA -lm"

# Use the native Autotools source lists and CLI entry point, with a JS output.
(cd "$repo" && bash autotools-init.sh)
mkdir -p "$build/intarna"
(cd "$build/intarna"
CXXFLAGS="$CXXFLAGS -Werror=inconsistent-missing-override" CPPFLAGS="-I$build/intarna/src -I$repo/src $CPPFLAGS" emconfigure "$repo/configure" --host=wasm32-unknown-emscripten \
mkdir -p "$intarna_build"
(cd "$intarna_build"
CXXFLAGS="$CXXFLAGS -Werror=inconsistent-missing-override" CPPFLAGS="-I$intarna_build/src -I$repo/src $CPPFLAGS" emconfigure "$repo/configure" --host=wasm32-unknown-emscripten \
--disable-multithreading --disable-log-coloring --with-mdspan=kokkos \
--with-vrna="$prefix" --with-boost="$prefix" \
--with-vrna="$vienna_prefix" --with-boost="$prefix" \
--with-boost-libdir="$prefix/lib" --with-zlib="$prefix"
emmake make -C src -j "$jobs" EXEEXT=.js \
LDFLAGS="$LDFLAGS -sMODULARIZE=1 -sEXPORT_ES6=1 -sEXPORT_NAME=createIntaRNA -sENVIRONMENT=web,worker,node -sINVOKE_RUN=0 -sEXIT_RUNTIME=1 -sEXPORTED_RUNTIME_METHODS=FS,callMain -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=1073741824 -sSTACK_SIZE=8388608"
Expand All @@ -96,5 +108,6 @@ mkdir -p "$build/intarna"
printf '{"type":"module"}\n' > "$build/site/wasm/package.json"
node "$repo/scripts/wasm/write-build-info.mjs" "$repo" "$build/site" \
"$(emcc -dumpversion)" "$(git -C "$repo" rev-parse HEAD)" \
"$(git -C "$repo" status --porcelain --untracked-files=no)"
"$(git -C "$repo" status --porcelain --untracked-files=no)" \
"$vienna_version" "$vienna_url" "$vienna_sha256"
echo "WASM assets: $build/site/wasm"
25 changes: 0 additions & 25 deletions scripts/wasm/patch-vienna.py

This file was deleted.

4 changes: 3 additions & 1 deletion scripts/wasm/smoke-protocol.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Integration coverage of browser argument translation against the real CLI.
import assert from 'node:assert/strict';
import { readFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
import { DEFAULTS, EVALUATION_EXAMPLE, prepareRequest, makeArguments } from '../../webinterface/protocol.js';
Expand All @@ -8,6 +9,7 @@ import { parseResults, projectCsv } from '../../webinterface/results.js';
import { archiveFiles } from '../../webinterface/archive.js';
const asset = resolve(process.argv[2] || 'build-wasm/site/wasm/intarna.js');
const { default: createIntaRNA } = await import(pathToFileURL(asset));
const compiledVersion = readFileSync(new URL('../build-info.txt', pathToFileURL(asset)), 'utf8');
async function run(input, format = 'C') {
const request = prepareRequest({ ...DEFAULTS, ...input });
const diagnostics = [];
Expand Down Expand Up @@ -54,7 +56,7 @@ async function replayArchive(input) {
const outputs = fs.readdir('/output').filter((name) => name !== '.' && name !== '..').map((name) => ({ name, text: fs.readFile(`/output/${name}`, { encoding: 'utf8' }) }));
const reports = [['normal', 'N'], ['detailed', 'D'], ['ensemble', 'E']].filter(([key]) => request.outputs.includes(key));
for (const [key, format] of reports) outputs.push({ name: `intarna-${key}.txt`, text: (await run(input, format)).text });
const files = archiveFiles(request, { csv: original.text, files: outputs }, 'IntaRNA 3.4.1\nViennaRNA 2.7.0\n');
const files = archiveFiles(request, { csv: original.text, files: outputs }, compiledVersion);
for (const config of ['parameters', ...reports.map(([key]) => key)]) {
const diagnostics = [];
const module = await createIntaRNA({ noInitialRun: true, locateFile: () => new URL('./intarna.wasm', pathToFileURL(asset)).href, print: (line) => diagnostics.push(line), printErr: (line) => diagnostics.push(line) });
Expand Down
28 changes: 23 additions & 5 deletions scripts/wasm/stage.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,24 @@ build=${WASM_BUILD_DIR:-"$repo/build-wasm"}
site="$build/site"
test -s "$site/wasm/intarna.js"
test -s "$site/wasm/intarna.wasm"
# Select notices from the archive used by these assets, including dlib upgrades.
read -r vienna_version vienna_sha256 dlib_directory < <(python3 - "$site" "$build" <<'PYSOURCE'
import json, re, sys
from pathlib import Path
site, build = map(Path, sys.argv[1:])
metadata = json.loads((site / "build-info.json").read_text())
archive = metadata["viennarnaArchive"]
version, digest = archive["version"], archive["sha256"]
if version != metadata["viennarna"] or not re.fullmatch(r"2\.\d+\.\d+", version) or not re.fullmatch(r"[0-9a-f]{64}", digest):
raise SystemExit("Invalid compiled ViennaRNA archive identity")
source = build / "sources" / f"vienna-{version}-{digest}" / f"ViennaRNA-{version}"
dlib = list((source / "src").glob("dlib-*/dlib/LICENSE.txt"))
if len(dlib) != 1 or not re.fullmatch(r"dlib-[0-9.]+", dlib[0].parent.parent.name):
raise SystemExit("Expected one bundled dlib license in the compiled ViennaRNA source")
print(version, digest, dlib[0].parent.parent.name)
PYSOURCE
)
vienna_source="$build/sources/vienna-$vienna_version-$vienna_sha256/ViennaRNA-$vienna_version"
mkdir -p "$site/licenses"
cp -R "$repo/webinterface/assets" "$site/"
for file in index.html styles.css app.js mark.svg protocol.js runner.js results.js options.js columns.js archive.js visualization.js; do
Expand All @@ -18,20 +36,20 @@ cp "$repo/src/easylogging++.LICENCE.txt" "$site/licenses/easylogging.txt"
cp "$repo/src/mdspan/LICENSE" "$site/licenses/mdspan.txt"
cp "$build/sources/boost_1_87_0/LICENSE_1_0.txt" "$site/licenses/Boost.txt"
cp "$build/sources/zlib-1.3.1/zlib.h" "$site/licenses/zlib.h"
cp "$build/sources/ViennaRNA-2.7.0/COPYING" "$site/licenses/ViennaRNA.txt"
cp "$build/sources/ViennaRNA-2.7.0/src/dlib-19.24/dlib/LICENSE.txt" "$site/licenses/dlib.txt"
cp "$build/sources/ViennaRNA-2.7.0/src/ViennaRNA/plotting/naview/naview.c" "$site/licenses/naview.c"
cp "$vienna_source/COPYING" "$site/licenses/ViennaRNA.txt"
cp "$vienna_source/src/$dlib_directory/dlib/LICENSE.txt" "$site/licenses/dlib.txt"
cp "$vienna_source/src/ViennaRNA/plotting/naview/naview.c" "$site/licenses/naview.c"
emscripten_dir=$(dirname "$(command -v emcc)")
cp "$emscripten_dir/LICENSE" "$site/licenses/Emscripten.txt"
cp "$emscripten_dir/system/lib/libc/musl/COPYRIGHT" "$site/licenses/musl.txt"
for library in libcxx libcxxabi libunwind compiler-rt; do
cp "$emscripten_dir/system/lib/$library/LICENSE.TXT" "$site/licenses/$library.txt"
done
cat > "$site/licenses/index.html" <<'HTML'
cat > "$site/licenses/index.html" <<HTML
<!doctype html><html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>IntaRNA licenses</title>
<h1>IntaRNA and third-party notices</h1>
<p>IntaRNA: Bioinformatics Group of Prof. Backofen, University of Freiburg.<br>ViennaRNA: authors and Institute for Theoretical Chemistry, University of Vienna.</p>
<p>This distribution uses ViennaRNA 2.7.0, Boost 1.87.0, zlib 1.3.1, bundled dlib 19.24 (with Unicode/template compatibility fixes), Kokkos mdspan, Easylogging++ and the Emscripten 4.0.23 runtime.</p>
<p>This distribution uses ViennaRNA $vienna_version, Boost 1.87.0, zlib 1.3.1, bundled dlib ${dlib_directory#dlib-}, Kokkos mdspan, Easylogging++ and the Emscripten 4.0.23 runtime.</p>
<ul>
<li><a href="IntaRNA.txt">IntaRNA</a></li><li><a href="ViennaRNA.txt">ViennaRNA</a></li>
<li><a href="Boost.txt">Boost</a></li><li><a href="zlib.h">zlib</a></li>
Expand Down
11 changes: 8 additions & 3 deletions scripts/wasm/test-browser.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import argparse
from pathlib import Path
from io import BytesIO
from urllib.parse import parse_qs, urlparse
import re
from urllib.parse import parse_qs, urljoin, urlparse
from zipfile import ZipFile
from playwright.sync_api import sync_playwright, expect

Expand Down Expand Up @@ -53,7 +54,11 @@ def predict():
expect(status).to_contain_text('complete:', timeout=120000)
expect(page.locator('#input-error')).not_to_be_visible()

expect(page.locator('#version-info')).to_contain_text('ViennaRNA 2.7.0')
metadata = page.request.get(urljoin(args.url, 'build-info.json')).json()
vienna_version = metadata['viennarna']
assert re.fullmatch(r'2\.\d+\.\d+', vienna_version)
assert metadata['viennarnaArchive']['version'] == vienna_version
expect(page.locator('#version-info')).to_contain_text(f'ViennaRNA {vienna_version}')
for logo in page.locator('.site-header img').all():
assert logo.evaluate('node => node.complete && node.naturalWidth > 0')
for key in ['query-options', 'target-options', 'seed-options', 'helix-options', 'shape-options', 'interaction-options', 'output-options', 'general-options', 'sequence-input']:
Expand Down Expand Up @@ -122,7 +127,7 @@ def predict():
assert archive['intarna-interactions.csv'] == csv
assert archive['query.fa'] == '>query\nGGGAGGAGGG\n'
assert '# IntaRNA 3.4.1' in archive['parameters.cfg']
assert 'Vienna RNA package 2.7.0' in archive['parameters.cfg']
assert f'Vienna RNA package {vienna_version}' in archive['parameters.cfg']
assert 'outNumber = 10' in archive['parameters.cfg']
assert 'IntaRNA --parameterFile=parameters.cfg' in archive['parameters.cfg']
assert 'temperature =' not in archive['parameters.cfg']
Expand Down
Loading
Loading