DEV Community

Cover image for How to Embed a Photino Project into a Single Executable with wrap_photino.py
artydev
artydev

Posted on

How to Embed a Photino Project into a Single Executable with wrap_photino.py

πŸ“¦ 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

  1. What Is This?
  2. How It Works (Deep Dive)
  3. Prerequisites
  4. Basic Usage
  5. Command-Line Reference
  6. Examples
  7. Output Structure
  8. Performance & Overhead
  9. Use Cases
  10. Troubleshooting
  11. How the Generated C Code Works
  12. Security Considerations
  13. 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 0o755 for the binary, 0o644 for 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:

  1. Embeds every file as a static byte array using a bin2c() converter.
  2. 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_PATH to 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.

Step 3 β€” Compilation

The generated C code is compiled with gcc:

gcc -O2 -s -o PhotinoWrapper photino_wrapper_XXXXXX.c
Enter fullscreen mode Exit fullscreen mode
  • -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         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

πŸš€ Basic Usage

python3 wrap_photino.py /path/to/photino/project
Enter fullscreen mode Exit fullscreen mode

This will:

  1. Scan /path/to/photino/project for PhotinoAOT, Photino.Native.so, and wwwroot/.
  2. Generate a C wrapper.
  3. 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]
Enter fullscreen mode Exit fullscreen mode
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
Enter fullscreen mode Exit fullscreen mode

Example 2: Custom Binary & Library Names

python3 wrap_photino.py ./publish \
    --binary MyApp \
    --native libphotino.so \
    --wwwroot assets \
    --output MyAppWrapper
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

⚠️ Note: This only statically links the wrapper itself, not the embedded Photino binary or its dependencies. The embedded Photino.Native.so is 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
Enter fullscreen mode Exit fullscreen mode

πŸ“ Output Structure

After running the script, you'll get a single file:

./PhotinoWrapper          # The bundled executable (e.g., 15 MB)
Enter fullscreen mode Exit fullscreen mode

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
    └── ...
Enter fullscreen mode Exit fullscreen mode

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%)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Fix: Specify the correct binary name with --binary:

python3 wrap_photino.py . --binary MyCustomBinary
Enter fullscreen mode Exit fullscreen mode

"compilation failed"

Error: compilation failed:
gcc: command not found
Enter fullscreen mode Exit fullscreen mode

Fix: Install GCC (see Prerequisites).

"no files to embed"

Error: no files to embed!
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

The application crashes at startup

  • Check that all required shared libraries are available on the target system.
  • Try running without --suppress-debug first 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;
Enter fullscreen mode Exit fullscreen mode

2. Helper Functions

  • write_file() β€” Writes binary data to a file and sets permissions.
  • mkdir_p() β€” Recursively creates directories (like mkdir -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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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-debug discards 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-debug with --static for 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()
Enter fullscreen mode Exit fullscreen mode

Top comments (0)