Skip to content

Bundling Windows applications

Iightspark edited this page Jun 19, 2022 · 18 revisions

Tools that can bundle Windows applications automatically

https://github.com/sudo-give-me-coffee/wine32-deploy


Ongoing research

⚠️ CAUTION: The information on this page reflects ongoing research. Do not rely on it yet. We would appreciate your experimentation and contributions. Feel free to edit the page or reach out to us in #AppImage on irc.freenode.net.

Objectives

User story: "I as the user want to download an AppImage that lets me run a Windows application on Linux, without needing to install anything."

  1. Create a version of WINE that runs from within an AppImage
  2. In order to support running 32-bit Windows executables on 64-bit Linux systems, make it possible to bundle any and all 32-bit dependencies, down to ld-linux.so.2 inside the AppImage so that it needs no 32-bit libraries to be installed on the target system
  3. Make it possible to use a read-only $WINEPREFIX that is stored inside the AppImage
  4. Minimize the AppImage

Plan of attack

1. Create a version of WINE that runs from within an AppImage

For 64-bit WINE on 64-bit Linux this is relatively straightforward. Here is an example, made by a third party, without our help: http://www.wildmediaserver.com/download.php (not developed by us) offers Linux versions which are basically Windows versions that bundle a minimal WINE subset.

But this approach will only be able to run 64-bit Windows executables, which are not always available, especially for legacy software that is often the reason for using WINE. Hence we will focus on 2.

2. Bundle any and all 32-bit dependencies

It appears that in order to run 32-bit Windows applications, 32-bit Windows must be used, which in turn requires 32-bit ld-linux.so.2 and glibc. But most 64-bit systems these days don't have the 32-bit compatibility libraries installed anymore.

With other programs it is usually possible to manually load the 32-bit ELF file with a private, bundled version of ld-linux.so.2 like so:

wget -c http://security.ubuntu.com/ubuntu/pool/main/g/glibc/libc6-i386_2.24-9ubuntu2.2_amd64.deb
dpkg -x ./libc6*.deb .

HERE="$(dirname "$(readlink -f "${0}")")"

WINEPREFIX="$HERE/wineprefix/" \
  "$HERE/lib32/ld-linux.so.2"  \
  --library-path "$HERE/lib32" \
  "$HERE/wine-stable/bin/wine" \
  "$WINEPREFIX/drive_c/Program Files/App/App.exe"  "$@"

However, with WINE, this does not work. My guess is that WINE launches other WINE instances through other mechanisms in the background, which in turn don't get loaded using the specified "$HERE/lib32/ld-linux.so.2" and --library-path "$HERE/lib32".

WINE developer Alexandre Julliard answered on wine-devel:

It's not possible in standard Wine, but you could probably hack wine_exec_wine_binary() in libs/wine/config.c to add your special magic.

🚧 This needs to be done. We would highly welcome contributions.

In the meantime, we may get around this limitation by placing a symlink to our custom ld-linux.so.2 at a fixed location such as /tmp, but it is an ugly hack.

3. Make it possible to use a read-only $WINEPREFIX

$WINEPREFIX, sometimes also called "WINE bottle", is the directory that contains the drive_c directory and configuration information for WINE. It is basically an isolated Windows environment that can hold one multiple applications together with the required dependencies, registry, etc. You can roughly think of it as a "simulated Windows hard disk".

Since Windows and Windows applications are not designed to run from read-only locations (such as AppImages), WINE checks for $WINEPREFIX to be writeable and owned by the currently running $USER. This makes it impossible to use the same $WINEPREFIX in multi-user setups. There are two possible solutions to this:

  • Prior to running WINE, copy $WINEPREFIX to the user's $HOME and change the permissions so that the directory is owned by $USER. This is what most other implementations do. However, for large Windows applications, this means consuming a lot of space, and it is not in line with the AppImage philosophy which mandates that applications are kept compressed and read-only inside the AppImage all the time. So we should not use this approach
  • Use an overlay/union filesystem (similar to what is used on Linux Live ISOs) to make $WINEPREFIX appear writeable and owned by $USER by redirecting writes to a different (temporary) location. We should use this approach

Actually, we had been looking at this already 10 years ago https://www.winehq.org/pipermail/wine-devel/2007-November/060627.html but now we have a solution that appears to work (see below).

4. Minimize the AppImage

Typically a WINE installation contains more files than a particular Windows executable requires during normal operation. Similarly, a typical Windows application installation contains more files than are needed for normal application execution (e.g., installers, uninstallers, updaters, temporary files etc). Hence, once we have a working $WINEPREFIX, we may want to reduce its footprint by removing everything that is not strictly needed for the execution of the application.

One way to achieve this is to monitor execution using tools like strace, and remove all files that are not opened.

Step-by-step instructions

⚠️ CAUTION: The information on this page reflects ongoing research. Do not rely on it yet. We would appreciate your experimentation and contributions. Feel free to edit the page or reach out to us in #AppImage on irc.freenode.net.

1. Create a version of WINE that runs from within an AppImage

For 64-bit Windows executables using 64-bit WINE on 64-bit Linux machines, it is rather straightforward. Section to be written. But since WINE is often used to run legacy 32-bit Windows executables, skip to the next section which is much more complex.

2. Bundle any and all 32-bit dependencies

Let's start by getting a 32-bit WINE build that is specifically made to run on many different distributions.

mkdir -p WINE/WINE.AppDir
cd WINE
wget -c https://www.playonlinux.com/wine/binaries/linux-x86/PlayOnLinux-wine-2.22-linux-x86.pol
cd WINE.AppDir
tar xf ../*.pol
mv wineversion/*/ usr/
rm -r files/ playonlinux/ wineversion/

Let's try to run it. We will remove the need for LD_LIBRARY_PATH in a later step.

export LD_LIBRARY_PATH=/tmp/i386-linux-gnu/:/tmp/lib/i386-linux-gnu/:$(readlink -f ./usr/lib/)
./usr/bin/winecfg 
# ./usr/bin/winecfg: 46: exec: ./usr/bin/wine: not found

We will remove the need for LD_LIBRARY_PATH in a later step.

The error is because on our system the 32-bit libraries are missing. We need to get them! The only question is, which ones do we need to install?

sudo apt-get install libfreetype6:i386 libxext6:i386 libxext6:i386 libudev1:i386 libncurses5:i386

Now it should run:

./usr/bin/winecfg 

Press "Cancel". Also in the subsequent tries.

We can apparently safely ignere these error messages:

err:module:load_builtin_dll failed to load .so lib for builtin L"winebus.sys": libudev.so.0: cannot open shared object file: No such file or directory
err:winedevice:async_create_driver failed to create driver L"WineBus": c0000142

Now we change the search path to ld-linux.so.2:

FILES=$(grep -r ld-linux.so.2 'usr/' | cut -d " " -f 3)

for FILE in $FILES ; do
  echo /isodevice/Applications/patchelf --set-interpreter /tmp/ld-linux.so.2 $FILE
  /isodevice/Applications/patchelf --set-interpreter /tmp/ld-linux.so.2 $FILE
done

cp /lib/ld-linux.so.2 /tmp/

Now it should still run:

./usr/bin/winecfg 

But there is a problem, it is still loading libraries from the system. We need to change that!

strings /tmp/ld-linux.so.2 | grep /lib
# /usr/lib/i386-linux-gnu/
# /lib/i386-linux-gnu/
# (...)

We need to patch ld-linux.so.2 so that it loads libraries from our location rather than from the system:

sed -i -e 's|/usr/lib/i386-linux-gnu/|/tmp/lib/i386-linux-gnu/|g' /tmp/ld-linux.so.2
sed -i -e 's|/lib/i386-linux-gnu/|/tmp/i386-linux-gnu/|g' /tmp/ld-linux.so.2

mkdir -p /tmp/i386-linux-gnu/
LIBS=$(strace ./usr/bin/winecfg 2>&1 | grep -E '"/lib/i386-linux-gnu' | cut -d '"' -f 2)
for LIB in $LIBS ; do
  echo cp $LIB /tmp/i386-linux-gnu/
  cp $LIB /tmp/i386-linux-gnu/
done

mkdir -p /tmp/lib/i386-linux-gnu/
LIBS=$(strace ./usr/bin/winecfg 2>&1 | grep -E '"/usr/lib/i386-linux-gnu' | cut -d '"' -f 2)
for LIB in $LIBS ; do
  echo cp $LIB /tmp/lib/i386-linux-gnu/
  cp $LIB /tmp/lib/i386-linux-gnu/
done

But it still loads stuff from the system as we can see:

strace -f ./usr/bin/winecfg 2>&1 | grep -E '"/lib/i386-linux-gnu|"/usr/lib/i386-linux-gnu'

That is bad, we must change that.

# Now disable loading from the system paths stored in /etc
sed -i -e 's|/etc/ld.so.cache|/xxx/ld.so.cache|g' /tmp/ld-linux.so.2

For some strange reason, I need to do manually:

cp /lib/i386-linux-gnu/libgcc_s.so.1 /tmp/i386-linux-gnu/

Why was it not copied above?

But it still loads gconv from the system as we can see:

strace -f ./usr/bin/winecfg 2>&1 | grep -E '"/lib/i386-linux-gnu|"/usr/lib/i386-linux-gnu'

That is bad, we must change that.

sed -i -e 's|/usr/lib/i386-linux-gnu/|/tmp/lib/i386-linux-gnu/|g' /tmp/i386-linux-gnu/libc.so.6

Now it should run without loading anything i386 from the system:

strace -f ./usr/bin/winecfg 2>&1 | grep -E '"/lib/i386-linux-gnu|"/usr/lib/i386-linux-gnu'

If it is still showing up something, then stop here.

Now let's move stuff away from /tmp and replace it by symlinks instead:

mkdir -p ./usr/tmp
mv /tmp/lib /tmp/i386-linux-gnu /tmp/ld-linux.so.2 ./usr/tmp/
( LOC=$(readlink -f ./usr/tmp/) ; cd /tmp ; ln -s $LOC/* . )

Now it should still run:

./usr/bin/winecfg 

Let's remove the need for LD_LIBRARY_PATH:

find usr/lib/wine/*.so* -exec /isodevice/Applications/patchelf --set-rpath '$ORIGIN:$ORIGIN/../:$ORIGIN/../../tmp/i386-linux-gnu:$ORIGIN/../../tmp/lib/i386-linux-gnu' {} \;
find usr/lib/*.so* -exec /isodevice/Applications/patchelf --set-rpath '$ORIGIN:$ORIGIN/../tmp/i386-linux-gnu:$ORIGIN/../tmp/lib/i386-linux-gnu' {} \;
find usr/tmp/i386-linux-gnu/*so* -exec /isodevice/Applications/patchelf --set-rpath '$ORIGIN' {} \;
find usr/tmp/lib/i386-linux-gnu/*so* -exec /isodevice/Applications/patchelf --set-rpath '$ORIGIN:$ORIGIN/..' {} \;

Now it should still run:

unset LD_LIBRARY_PATH
./usr/bin/winecfg 

After we are done, we can remove the symlinks:

rm /tmp/lib /tmp/i386-linux-gnu /tmp/ld-linux.so.2

Congratulations. We now seem to have an installation of WINE that is reasonably decoupled from the system and could run on another Linux system with does not have the 32-bit libraries installed.

What is missing is an AppRun file that sets up the symlinks in /tmp/, runs WINE, and cleans up again.

cat > AppRun <<\\EOF
#!/bin/bash

HERE="$(dirname "$(readlink -f "${0}")")"

if [ -L /tmp/ld-linux.so.2 ] ; then
  echo "Cannot run due to existing symlinks in /tmp (FIXME)"
  exit 1
fi

LOC=$(readlink -f "$HERE"/usr/tmp/)
( cd /tmp ; ln -s "$LOC"/{lib,i386-linux-gnu,ld-linux.so.2} . )

if [ -z "$1" ] ; then
  "$HERE"/usr/bin/wine winecfg
else
  "$HERE"/usr/bin/wine "$@"
fi

sleep 10 # Workaround for: libgcc_s.so.1 must be installed for pthread_cancel to work
rm /tmp/lib /tmp/i386-linux-gnu /tmp/ld-linux.so.2
EOF
chmod +x AppRun

Verify that it runs:

chmod +x AppRun winecfg

We can now pack our standalone WINE as an AppImage.

mkdir -p usr/share/icons/hicolor/256x256/apps/ ; wget -c "https://dl2.macupdate.com/images/icons256/17376.png" -O usr/share/icons/hicolor/256x256/apps/wine.png
sed -i -e 's|^NoDisplay=true|NoDisplay=false|g' ./usr/share/applications/wine.desktop
sed -i -e 's|^Name=.*|Name=WINE|g' ./usr/share/applications/wine.desktop
echo "Categories=System;Emulator;" >> ./usr/share/applications/wine.desktop
cp ./usr/share/applications/wine.desktop .
cp ./usr/share/icons/hicolor/256x256/apps/wine.png .
cd ..
wget https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage
chmod +x appimagetool-x86_64.AppImage
ARCH=x86_64 ./appimagetool-x86_64.AppImage WINE.AppDir/

Seems to run cleanly! We now have an AppImage that can run 32-bit WINE on 64-bit Linux machines without requiring any 32-bit libraries to be installed on the target machine.

⚠️ CAUTION: This currently seems to work only for WINE itself and the tiny applications that come with it (like notepad), but not for complex real-world applications such as QQ.exe. More research is required. It is only halfway working when moved over to another system. Depending on the dlls used by a payload application, we may need to pack more 32-bit libraries. Contributions welcome!

3. Make it possible to use a read-only $WINEPREFIX

In order to be able to use a read-only $WINEPREFIX stored inside the AppImage and not owned by the currently running user, we need to use an overlay/union filesystem (similar to what is used on Linux Live ISOs) to make $WINEPREFIX appear writeable and owned by $USER by redirecting writes to a different (temporary) location.

wget -c http://mirrors.kernel.org/ubuntu/pool/universe/u/unionfs-fuse/unionfs-fuse_0.24-2.2ubuntu1_amd64.deb
dpkg -x ./unionfs-fuse*.deb . ; rm ./unionfs-fuse*.deb

Need to run the application at least once from the read/write $WINEPREFIX so that WINE can configure it and will not say "update wine configuration" on each start. During that run, manually press "cancel" on all installations.

Next, we need to stop WINE from updating $WINEPREFIX automatically from time to time:

echo "disable" > "$WINEPREFIX/.update-timestamp" 

Finally, we need to use special magic in the AppRun script to use unionfs-fuse:

cat > AppRun <<\EOF
#!/bin/bash
HERE="$(dirname "$(readlink -f "${0}")")"
RO_WINEPREFIX="$HERE/opt/cxoffice/support/apps.com.qq.im.light/" # Use the location of the WINEPREFIX in the AppDir
MNT_WINEPREFIX="$HOME/.QQ.unionfs" # Use the name of the app
TMP_WINEPREFIX_OVERLAY=/tmp/QQ # Use the name of the app
export LD_LIBRARY_PATH=./lib32/:./usr./lib32/:./lib/i386-linux-gnu/:./usr/lib/i386-linux-gnu/:$LD_LIBRARY_PATH
mkdir -p "$MNT_WINEPREFIX" "$TMP_WINEPREFIX_OVERLAY"
$HERE/usr/bin/unionfs-fuse -o use_ino,nonempty,uid=$UID -ocow "$TMP_WINEPREFIX_OVERLAY"=RW:"$RO_WINEPREFIX"=RO "$MNT_WINEPREFIX" || exit 1
WINEPREFIX="$MNT_WINEPREFIX" \
"$HERE/lib32/ld-linux.so.2" --library-path "$HERE/lib32" "$HERE"/opt/wine-stable/bin/wine \
"$MNT_WINEPREFIX/drive_c/Program Files/Tencent/QQLite/Bin/QQ.exe" "$@"
killall $HERE/usr/bin/unionfs-fuse && rm -r "$MNT_WINEPREFIX" # TODO: Move to atexit() function and trap it
EOF
chmod +x AppRun

4. Minimize the AppImage

Once everything runs well and has been tested thoroughly, we can remove files that are not necessary for the execution of the payload Windows executable in order to save some space. Section to be written

⚠️ CAUTION: The information on this page reflects ongoing research. Do not rely on it yet. We would appreciate your experimentation and contributions. Feel free to edit the page or reach out to us in #AppImage on irc.freenode.net.

References

Clone this wiki locally