π¦ wrap_photino.py β Photino AOT Single-File Bundler
look at bottom for the code.
Bundle an entire Photino AOT desktop application into a single, standalone executable β with optional debug-output suppression.
Table of Contents
- What Is This?
- How It Works (Deep Dive)
- Prerequisites
- Basic Usage
- Command-Line Reference
- Examples
- Output Structure
- Performance & Overhead
- Use Cases
- Troubleshooting
- How the Generated C Code Works
- Security Considerations
- License
π§ What Is This?
wrap_photino.py is a Python build-time tool that takes a compiled Photino AOT (Ahead-of-Time) desktop application β consisting of a native binary, a shared library (.so), and a wwwroot web assets folder β and bundles everything into a single, portable executable.
The resulting executable:
- β Self-contained β no external files needed at runtime.
- β Portable β extract-and-run on any compatible Linux system.
- β Clean β automatically cleans up temporary files after exit.
- β Silent β can optionally suppress both stdout and stderr (ideal for kiosks, embedded systems, or production deployments).
βοΈ How It Works (Deep Dive)
Step 1 β File Discovery & Embedding
The script scans the project directory for three categories of files:
| Component | Default Path | Purpose |
|---|---|---|
| Main binary | PhotinoAOT |
The compiled .NET AOT executable |
| Native library | Photino.Native.so |
The Photino native bridge (WebView backend) |
| Web root | wwwroot/ |
HTML, CSS, JS, images, and other frontend assets |
Each file is read into memory as raw bytes. The script then:
- Tracks file permissions (executable
0o755for the binary,0o644for everything else). - Calculates total embedded payload size.
- Reports each file and its size to the console.
Step 2 β C Wrapper Generation
The script generates a single C source file that:
-
Embeds every file as a static byte array using a
bin2c()converter. -
Generates a
main()function that:- Creates a temporary directory (
/tmp/photino_wrapper_XXXXXX). - Recreates the original directory structure inside it.
- Writes all embedded files to disk with correct permissions.
- Sets
LD_LIBRARY_PATHto include the temp directory. - Changes the working directory to the temp directory.
- Forks a child process to run the actual application.
- Waits for the child to finish.
- Cleans up all temporary files and directories.
- Returns the child's exit code.
- Creates a temporary directory (
Step 3 β Compilation
The generated C code is compiled with gcc:
gcc -O2 -s -o PhotinoWrapper photino_wrapper_XXXXXX.c
-
-O2β Optimise for speed/size. -
-sβ Strip debug symbols from the final binary. -
--static(optional) β Link everything statically for maximum portability.
Step 4 β Runtime Behaviour
When the wrapper is executed:
βββββββββββββββββββββββββββββββββββββββββββββββββββ
β ./PhotinoWrapper β
β β
β 1. Create /tmp/photino_wrapper_XXXXXX β
β 2. Write all embedded files β
β 3. Set LD_LIBRARY_PATH β
β 4. chdir to temp dir β
β 5. fork() β
β ββ CHILD: execv("PhotinoAOT", argv) β
β ββ PARENT: waitpid(), cleanup, exit β
βββββββββββββββββββββββββββββββββββββββββββββββββββ
If --suppress-debug is enabled, the child process has its stdout and stderr redirected to /dev/null before execv, silencing all output.
β Prerequisites
| Requirement | Notes |
|---|---|
| Python 3.6+ | For running the script |
| GCC | To compile the generated C wrapper |
| Linux | The wrapper uses Linux-specific APIs (fork, execv, mkdtemp, dup2) |
| A compiled Photino AOT project | Must contain the binary, .so, and wwwroot/
|
Install GCC if missing:
# Debian / Ubuntu
sudo apt install build-essential
# Fedora / RHEL
sudo dnf install gcc
# Arch Linux
sudo pacman -S gcc
π Basic Usage
python3 wrap_photino.py /path/to/photino/project
This will:
- Scan
/path/to/photino/projectforPhotinoAOT,Photino.Native.so, andwwwroot/. - Generate a C wrapper.
- Compile it into
./PhotinoWrapper.
π Command-Line Reference
usage: wrap_photino.py [-h] [--output OUTPUT] [--binary BINARY]
[--native NATIVE] [--wwwroot WWWROOT]
[--static] [--keep-c] [--suppress-debug]
[project_dir]
| Argument | Short | Default | Description |
|---|---|---|---|
project_dir |
β |
. (current dir) |
Path to the compiled Photino project |
--output |
-o |
PhotinoWrapper |
Name of the output executable |
--binary |
-b |
PhotinoAOT |
Name of the main AOT binary inside the project |
--native |
-n |
Photino.Native.so |
Name of the native shared library |
--wwwroot |
-w |
wwwroot |
Name of the web assets directory |
--static |
-s |
off | Statically link the wrapper (adds -static to GCC) |
--keep-c |
β | off | Keep the intermediate .c file after compilation |
--suppress-debug |
β | off | Redirect both stdout and stderr to /dev/null at runtime |
π§ͺ Examples
Example 1: Minimal Bundle
# Navigate to your published Photino project
cd ~/my-app/publish
# Bundle everything
python3 ~/tools/wrap_photino.py .
# Run the result
./PhotinoWrapper
Example 2: Custom Binary & Library Names
python3 wrap_photino.py ./publish \
--binary MyApp \
--native libphotino.so \
--wwwroot assets \
--output MyAppWrapper
Example 3: Suppress All Debug Output
Perfect for production deployments, kiosks, or CI/CD pipelines where console noise is unwanted:
python3 wrap_photino.py ./publish --suppress-debug
Now ./PhotinoWrapper will run completely silently β no stdout, no stderr.
Example 4: Fully Static Build
For maximum portability across Linux distributions (no dependency on specific shared library versions):
python3 wrap_photino.py ./publish --static
β οΈ Note: This only statically links the wrapper itself, not the embedded Photino binary or its dependencies. The embedded
Photino.Native.sois still dynamically loaded.
Example 5: Keep the Intermediate C File
If you want to inspect or manually tweak the generated C code:
python3 wrap_photino.py ./publish --keep-c
# The .c file will be in /tmp/ or the current directory
π Output Structure
After running the script, you'll get a single file:
./PhotinoWrapper # The bundled executable (e.g., 15 MB)
At runtime, the wrapper recreates this structure inside /tmp/photino_wrapper_XXXXXX/:
/tmp/photino_wrapper_XXXXXX/
βββ PhotinoAOT # Main binary (executable)
βββ Photino.Native.so # Native library
βββ wwwroot/ # Web assets
βββ index.html
βββ style.css
βββ app.js
βββ ...
Everything is automatically deleted when the application exits.
π Performance & Overhead
The script reports overhead after compilation:
Total embedded data: 14,582,880 bytes
Overhead: 48,128 bytes (0.3%)
The overhead is typically < 1% and comes from:
- The C wrapper code itself.
- ELF headers and section alignment.
- GCC optimisation padding.
The startup cost is minimal β just fork() + file writes to a tmpfs (usually RAM-backed).
π― Use Cases
| Scenario | Why This Tool |
|---|---|
| Distribution | Ship a single file instead of a directory tree |
| Kiosk / Embedded |
--suppress-debug keeps the display clean |
| CI/CD Pipelines | Single artifact, easy to copy and deploy |
| End-User Simplicity | No "extract to folder" step β just run |
| Cross-Platform Packaging | Combine with AppImage or similar |
| Security | No writable application directory needed at runtime |
π§ Troubleshooting
"main binary not found"
Error: main binary not found: ./PhotinoAOT
Fix: Specify the correct binary name with --binary:
python3 wrap_photino.py . --binary MyCustomBinary
"compilation failed"
Error: compilation failed:
gcc: command not found
Fix: Install GCC (see Prerequisites).
"no files to embed"
Error: no files to embed!
Fix: Make sure your project directory contains at least the main binary. The native library and wwwroot are optional.
Permission denied when running the wrapper
chmod +x PhotinoWrapper
./PhotinoWrapper
The application crashes at startup
- Check that all required shared libraries are available on the target system.
- Try running without
--suppress-debugfirst to see error messages. - Verify the embedded binary works standalone:
./PhotinoAOT(from the original project dir).
𧬠How the Generated C Code Works
The generated C file is a self-extracting archive. Here's the anatomy:
1. Embedded Data
Each file becomes a static byte array:
static unsigned char file_PhotinoAOT[] = {
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
// ... thousands of bytes ...
};
static unsigned int file_PhotinoAOT_len = 14582880;
2. Helper Functions
-
write_file()β Writes binary data to a file and sets permissions. -
mkdir_p()β Recursively creates directories (likemkdir -p). -
cleanup_files()β Removes all extracted files and directories in reverse order.
3. Main Logic
int main(int argc, char *argv[]) {
// 1. Create temp directory
mkdtemp("/tmp/photino_wrapper_XXXXXX");
// 2. Write all embedded files
write_file("...", file_xxx, file_xxx_len, 0755);
// 3. Set LD_LIBRARY_PATH
setenv("LD_LIBRARY_PATH", tmpdir, 1);
// 4. chdir to temp directory
chdir(tmpdir);
// 5. Fork and execute
pid = fork();
if (pid == 0) {
// Child: optionally suppress output, then exec
if (suppress_debug) {
dup2(devnull, STDOUT_FILENO);
dup2(devnull, STDERR_FILENO);
}
execv("PhotinoAOT", argv);
} else {
// Parent: wait, cleanup, return exit code
waitpid(pid, &status, 0);
cleanup_files(tmpdir);
return exit_code;
}
}
Why fork()?
The wrapper uses fork() so that:
- The parent process can clean up the temporary directory after the child exits.
- The child inherits the modified environment (
LD_LIBRARY_PATH, working directory). - The child's exit code is propagated back to the caller.
π Security Considerations
-
Temporary files are created in
/tmp/with a random suffix (XXXXXX), making name collisions extremely unlikely. -
Cleanup is guaranteed β the parent process always calls
cleanup_files()after the child exits, regardless of the child's exit status. - No setuid/setgid bits are preserved on extracted files.
-
--suppress-debugdiscards all output β useful for hiding sensitive paths or debug info in production.
β οΈ Note: The embedded binary runs with the same privileges as the user who launches the wrapper. There is no sandboxing.
π License
This script is provided as-is. Use it freely in your own projects.
π Summary
| Feature | Supported |
|---|---|
| Single-file output | β |
| Preserves file permissions | β |
| Automatic cleanup | β |
| Debug output suppression | β |
| Static linking (wrapper) | β |
| Custom file names | β |
| Cross-distro portable | β
(with --static) |
| Windows / macOS | β (Linux only) |
Pro tip: Combine
--suppress-debugwith--staticfor a production-ready, silent, portable kiosk application in one command:python3 wrap_photino.py ./publish --static --suppress-debug -o MyKioskApp
wrap_photino.py
#!/usr/bin/env python3
"""
wrap_photino.py - Bundle Photino AOT into single executable with debug suppression.
Final version: suppresses both stdout and stderr.
Usage:
python3 wrap_photino_final.py <project_dir> [--output <name>] [--suppress-debug]
"""
import os
import sys
import subprocess
import tempfile
import argparse
def bin2c(data, varname):
"""Convert binary data to a C byte array."""
lines = [f"static unsigned char {varname}[] = {{"]
for i in range(0, len(data), 12):
chunk = data[i:i+12]
hex_bytes = ", ".join(f"0x{b:02x}" for b in chunk)
lines.append(f" {hex_bytes},")
lines.append("};")
lines.append(f"static unsigned int {varname}_len = {len(data)};")
return "\n".join(lines)
def generate_c_wrapper(files, suppress_debug=False):
"""Generate C source code for wrapper."""
parts = []
parts.append("""/*
* Photino Single-File Wrapper
* Suppresses both stdout and stderr if --suppress-debug is used
*/
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <libgen.h>
#include <fcntl.h>
""")
# Generate embedded data
var_names = {}
for rel_path, (data, mode) in sorted(files.items()):
varname = "file_" + rel_path.replace("/", "_").replace(".", "_").replace("-", "_")
var_names[rel_path] = (varname, mode)
parts.append(bin2c(data, varname))
parts.append("")
parts.append("""
static int write_file(const char *path,
const unsigned char *data,
unsigned int len, int mode)
{
FILE *f = fopen(path, "wb");
if (!f) {
fprintf(stderr, "Error: cannot write %s: %s\\n",
path, strerror(errno));
return -1;
}
if (fwrite(data, 1, len, f) != len) {
fprintf(stderr, "Error: short write to %s\\n", path);
fclose(f);
return -1;
}
fclose(f);
if (chmod(path, mode) != 0) {
fprintf(stderr, "Error: chmod %s: %s\\n",
path, strerror(errno));
return -1;
}
return 0;
}
static int mkdir_p(const char *dir) {
char tmp[4096];
char *p = NULL;
size_t len;
snprintf(tmp, sizeof(tmp), "%s", dir);
len = strlen(tmp);
if (tmp[len - 1] == '/')
tmp[len - 1] = 0;
for (p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = 0;
mkdir(tmp, 0755);
*p = '/';
}
}
return mkdir(tmp, 0755);
}
static void cleanup_files(const char *tmpdir)
{
char path[4096];
""")
# Cleanup code
parts.append(" /* Clean up all embedded files */")
for rel_path in reversed(sorted(files.keys(), key=lambda x: -x.count('/'))):
parts.append(f' snprintf(path, sizeof(path), "%s/{rel_path}", tmpdir);')
parts.append(' (void)unlink(path);')
dirs = set()
for rel_path in files:
d = os.path.dirname(rel_path)
if d:
dirs.add(d)
parts.append(" /* Clean up directory structure */")
for d in reversed(sorted(dirs, key=lambda x: -x.count('/'))):
parts.append(f' snprintf(path, sizeof(path), "%s/{d}", tmpdir);')
parts.append(' (void)rmdir(path);')
parts.append(' (void)rmdir(tmpdir);')
parts.append('}')
parts.append('')
parts.append("""int main(int argc, char *argv[]) {
char tmpdir[4096];
char path[4096];
char *env;
char new_ld_path[8192];
const char *main_binary;
pid_t pid;
int status;
int devnull = -1;
/* Create a temporary directory */
snprintf(tmpdir, sizeof(tmpdir), "/tmp/photino_wrapper_XXXXXX");
if (mkdtemp(tmpdir) == NULL) {
fprintf(stderr, "Error: mkdtemp failed: %s\\n", strerror(errno));
return 1;
}
""")
# Find main binary
main_binary_name = None
for rel_path, (data, mode) in sorted(files.items()):
if mode & 0o111:
main_binary_name = rel_path
break
if not main_binary_name:
main_binary_name = "PhotinoAOT"
# Collect directories
dirs = set()
for rel_path in files:
d = os.path.dirname(rel_path)
if d:
dirs.add(d)
# Create directories
parts.append(" /* Create directory structure */")
for d in sorted(dirs):
parts.append(f' snprintf(path, sizeof(path), "%s/{d}", tmpdir);')
parts.append(' mkdir_p(path);')
parts.append("")
# Write files
parts.append(" /* Write all files */")
for rel_path, (data, mode) in sorted(files.items()):
varname, file_mode = var_names[rel_path]
parts.append(f' snprintf(path, sizeof(path), "%s/{rel_path}", tmpdir);')
parts.append(f' if (write_file(path, {varname}, {varname}_len, 0{oct(mode)[2:]}) != 0) {{')
parts.append(' cleanup_files(tmpdir);')
parts.append(' return 1;')
parts.append(' }')
parts.append("")
# Set LD_LIBRARY_PATH
parts.append(" /* Set LD_LIBRARY_PATH */")
parts.append(' env = getenv("LD_LIBRARY_PATH");')
parts.append(' if (env && env[0]) {')
parts.append(' snprintf(new_ld_path, sizeof(new_ld_path), "%s:%s", tmpdir, env);')
parts.append(' } else {')
parts.append(' snprintf(new_ld_path, sizeof(new_ld_path), "%s", tmpdir);')
parts.append(' }')
parts.append(' setenv("LD_LIBRARY_PATH", new_ld_path, 1);')
parts.append("")
# Change directory
parts.append(" /* Change to temp directory */")
parts.append(' if (chdir(tmpdir) != 0) {')
parts.append(' fprintf(stderr, "Error: chdir to %s: %s\\n", tmpdir, strerror(errno));')
parts.append(' cleanup_files(tmpdir);')
parts.append(' return 1;')
parts.append(' }')
parts.append("")
# If suppressing debug, open /dev/null now (before fork)
if suppress_debug:
parts.append(' /* Suppress output: open /dev/null */')
parts.append(' devnull = open("/dev/null", O_WRONLY);')
parts.append(' if (devnull < 0) {')
parts.append(' devnull = open("/dev/null", O_WRONLY | O_CREAT, 0666);')
parts.append(' }')
parts.append('')
# Fork and execute
parts.append(' /* Fork child process */')
parts.append(' pid = fork();')
parts.append(' if (pid < 0) {')
parts.append(' fprintf(stderr, "Error: fork failed: %s\\n", strerror(errno));')
parts.append(' cleanup_files(tmpdir);')
parts.append(' return 1;')
parts.append(' }')
parts.append('')
parts.append(' if (pid == 0) {')
parts.append(' /* Child process */')
if suppress_debug:
parts.append(' /* Suppress BOTH stdout and stderr */')
parts.append(' if (devnull >= 0) {')
parts.append(' fflush(stdout);')
parts.append(' fflush(stderr);')
parts.append(' dup2(devnull, STDOUT_FILENO); /* Redirect stdout */')
parts.append(' dup2(devnull, STDERR_FILENO); /* Redirect stderr */')
parts.append(' close(devnull);')
parts.append(' }')
parts.append('')
parts.append(f' execv("{main_binary_name}", argv);')
parts.append(' fprintf(stderr, "Error: execv failed: %s\\n", strerror(errno));')
parts.append(' exit(127);')
parts.append(' } else {')
parts.append(' /* Parent process */')
if suppress_debug:
parts.append(' if (devnull >= 0) close(devnull);')
parts.append(' int exit_code;')
parts.append(' waitpid(pid, &status, 0);')
parts.append(' exit_code = WIFEXITED(status) ? WEXITSTATUS(status) : 1;')
parts.append(' cleanup_files(tmpdir);')
parts.append(' return exit_code;')
parts.append(' }')
parts.append('}')
return "\n".join(parts)
def main():
parser = argparse.ArgumentParser(
description="Bundle Photino AOT project into single executable."
)
parser.add_argument("project_dir", nargs="?", default=".",
help="Directory containing Photino project files")
parser.add_argument("--output", "-o", default=None,
help="Output executable name (default: PhotinoWrapper)")
parser.add_argument("--binary", "-b", default="PhotinoAOT",
help="Name of main executable (default: PhotinoAOT)")
parser.add_argument("--native", "-n", default="Photino.Native.so",
help="Name of native library (default: Photino.Native.so)")
parser.add_argument("--wwwroot", "-w", default="wwwroot",
help="Name of web root directory (default: wwwroot)")
parser.add_argument("--static", "-s", action="store_true",
help="Statically link wrapper")
parser.add_argument("--keep-c", action="store_true",
help="Keep generated .c file")
parser.add_argument("--suppress-debug", action="store_true",
help="Suppress ALL output (stdout + stderr)")
args = parser.parse_args()
project_dir = args.project_dir
files = {}
# Main binary
binary_path = os.path.join(project_dir, args.binary)
if not os.path.isfile(binary_path):
print(f"Error: main binary not found: {binary_path}", file=sys.stderr)
sys.exit(1)
with open(binary_path, "rb") as f:
files[args.binary] = (f.read(), 0o755)
print(f" {args.binary}: {len(files[args.binary][0])} bytes")
# Native library
native_path = os.path.join(project_dir, args.native)
if os.path.isfile(native_path):
with open(native_path, "rb") as f:
files[args.native] = (f.read(), 0o644)
print(f" {args.native}: {len(files[args.native][0])} bytes")
# wwwroot
wwwroot_path = os.path.join(project_dir, args.wwwroot)
if os.path.isdir(wwwroot_path):
for root, dirs, filenames in os.walk(wwwroot_path):
for fn in filenames:
full_path = os.path.join(root, fn)
rel_path = os.path.relpath(full_path, project_dir)
with open(full_path, "rb") as f:
files[rel_path] = (f.read(), 0o644)
print(f" {rel_path}: {len(files[rel_path][0])} bytes")
if not files:
print("Error: no files to embed!", file=sys.stderr)
sys.exit(1)
total_size = sum(len(d) for d, _ in files.values())
print(f"\nTotal embedded data: {total_size} bytes")
print("\nGenerating C wrapper...")
c_source = generate_c_wrapper(files, args.suppress_debug)
output_name = args.output or "PhotinoWrapper"
c_fd, c_path = tempfile.mkstemp(suffix=".c", prefix="photino_wrapper_")
os.close(c_fd)
with open(c_path, "w") as f:
f.write(c_source)
print(f"Generated C source: {len(c_source)} bytes")
if args.suppress_debug:
print("Output suppression: ENABLED (stdout + stderr)")
print("Compiling...")
compile_cmd = ["gcc", "-O2", "-s", "-o", output_name, c_path]
if args.static:
compile_cmd.append("-static")
result = subprocess.run(compile_cmd, capture_output=True, text=True)
if result.returncode != 0:
print(f"Error: compilation failed:\n{result.stderr}", file=sys.stderr)
os.unlink(c_path)
sys.exit(1)
if not args.keep_c:
os.unlink(c_path)
wrapper_size = os.path.getsize(output_name)
overhead = wrapper_size - total_size
print(f"\nOutput: {output_name} ({wrapper_size} bytes)")
print(f"Overhead: {overhead} bytes ({overhead * 100 / total_size:.1f}%)")
print("Done!")
if __name__ == "__main__":
main()
Top comments (0)