-
-
Notifications
You must be signed in to change notification settings - Fork 563
Bundling Windows applications
https://github.com/sudo-give-me-coffee/wine32-deploy
#AppImage
on irc.freenode.net
.
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."
- Create a version of WINE that runs from within an AppImage
- 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
- Make it possible to use a read-only
$WINEPREFIX
that is stored inside the AppImage - Minimize the 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.
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()
inlibs/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.
$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).
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.
#AppImage
on irc.freenode.net
.
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.
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.
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!
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
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
#AppImage
on irc.freenode.net
.
- https://forum.winehq.org/viewtopic.php?f=8&t=29841
- "Add LD Loader Root Path Environment" WINE patch by Hackerl https://github.com/Hackerl/wine/commit/c609773f7d8e34dc263352a59943fc9a2358646a, discussed at https://github.com/Hackerl/Wine_Appimage/issues/11#issuecomment-445545841