Thursday, April 14, 2016

Fun experiences using Wine in Docker (part 2)

After the last post about running Wine in Docker, it was time to try and actually use the image to perform a build.

The first time I tried, the build crashed due to some sort of exception. It turns out the following sequence of events was to blame:

  • NMAKE, running under wine, loads msvcrt80.dll
  • During its DllMain, this DLL calls _wfindfirst64i32(), passing it the path to Microsoft.VC80.CRT.mainfest
  • Internally, _wfindfirst64i32 will:
    • Call FindFirstFileW which returns a WIN32_FIND_DATAW structure, which includes a FILETIME member for each of creation, last access, and last write times.
    • Pass each of those timestamps to a function that:
      • Calls FileTimeToLocalFileTime to convert it to local time
      • Calls FileTimeToSystemTime to convert it to a SYSTEMTIME structure
      • Passes each member of the SYSTEMTIME structure as arguments to another function, which raises an INVALID_PARAMETER exception (0xC000000D) if the Year argument is not between 1970 and 3000, inclusive

When Docker, using its union filesystem, starts the container, the file access times are zero, which is midnight, 1970-01-01. When this date is converted to local time (in EST timezone, which is UTC-5), the timestamp is five hours before midnight, 1970-01-01, which puts the year at 1969. This caused an exception to be raised whenever NMAKE would run.

The solution was quite simple: Removing /etc/localtime made the system use UTC time, which avoids the problem.

(When I find my notes, I will explain how I leveraged WINE's debugging facilities to track down this very elusive problem.

Wednesday, April 13, 2016

Fun experiences using Wine in Docker

Background

I sometimes work with a legacy codebase that targets both Windows and Linux; the build system is GNU Make-based, and builds on Linux. For the Windows components, the build system invokes NMAKE, using Wine. Yes, it's messy; yes I want to replace it; but no there's no time budgeted right now.

Lately, I've been moving more and more of our build infrastructure to Docker. It makes keeping the build environments up-to-date for developers easier, and simplifies the setup for Continuous Integration. Check out my tool, Scuba for using Docker to perform local builds, and GitLab CI.

You can see where this is going. I decided to convert our legacy build VM into a Docker image; Wine and NMAKE included. I didn't know what I was getting myself into.

VM to Docker Image

Of course, the right way to create a Docker image is to use a Dockerfile. However, this current VM had experienced years of tweaks, potentially relying on subtle toolchain-version-specific quirks. I wasn't about to re-build it from scratch, so I decided to convert the VM filesystem directly to a Docker image.

The initial conversion turned out to be straightforward. First, I cloned the VM, so I could work destructively. Next, I uninstalled everything that wasn't necessary for a Docker image (including KDE, X11, firewall, etc.) Then, I powered down the cloned VM, and mounted its virtual disk under another VM, running Docker. From there, it's as simple as using Tar to create the Docker image:

# cd /mnt/buildvm; tar -c * | docker import --change='CMD /bin/bash' - buildsys:1

This adds all of the directories from the mounted build VM disk, and creates a tar stream which is piped into docker import - (where - means standard input). Note that I'm also setting the `CMD` to be `/bin/bash`; this way, the image can be run by simply using docker run -it buildsys:1, without having to specify /bin/bash every run.

After the initial conversion was done and I no longer needed to "boot" in the conventional way, I continued to run the image, removing more stuff, like:

  • rpm -e --nodeps kernel-xxx (You don't need a kernel when running under Docker, but don't want to remove other things that "depend" on it.)
  • yum remove dracut grub plymouth
  • yum clean all && rm -rf /var/cache/yum
  • rm -rf /var/log/* /tmp/*
I definitely had to be careful not to remove things that Wine unexpectedly relied upon. As I did this, I occasionally ran the image through a docker export / docker import cycle to actually reduce the virtual size of the image.

Wine without X11

The first time I tried to run wine in a Docker container, I was met with the following warnings/errors:

Application tried to create a window, but no driver could be loaded.
Make sure that your X server is running and that $DISPLAY is set correctly.
Googling for the error yielded some results from some other guys crazy enough to try using Wine in Docker also, like this SuperUser post and this GitHub project. It seemed that I would need some sort of X server after all, and that Xvfb (X Virtual FrameBuffer) was the solution.

You can simply run xvfb-run wine whatever.exe, and this will avoid the "no $DISPLAY" problems. Great. However, I didn't want to change any of our code to have to run under Docker. Specifically, I didn't want to track down every invocation of wine and prefix it with xvfb-run; what if we are running on native X?

Instead, I came up with what I believe is a novel solution: ENTRYPOINT. This essentially prefixes the user's command with whatever is specified in ENTRYPOINT - just what we want to do with xvfb-run. So the last time I re-imported the tarball, I added --change='ENTRYPOINT xvfb-run'. There's probably a way to do this after it's been imported, but this was the most convenient at the time.

Now, when I run docker run --rm -it buildsys:1 /bin/bash, I can verify that $DISPLAY is set, and Wine is happy. For now.

More to come...