I had long been using Cygwin as my main operating environment when I needed to use Windows—which helped me keep my sanity level above zero. A little while ago, I migrated from Cygwin to one of its derivatives: MSYS2. Why?

  1. Cygwin’s package manager (the setup.exe) wasn’t nice, to put it nicely.
  2. MSYS2’s package manager, pacman, was one of the low-headache package managers I liked.
  3. I frequently needed to interact with native Windows executables from the command line. Famous ones include go, node.js, and rust, but there were also obscure things like Kerbal Space Program or Monster Hunter modding tools—these would be difficult to interface with from WSL. Cygwin didn’t do too well in this respect—I had a lot of wrapper scripts to convert paths and things.

However, it wasn’t exactly a no-brainer. There were pros and cons—MSYS2 was designed with a very different purpose in mind from Cygwin, after all. This page from MSYS2 wiki describes this in a nice summary (emphasis mine):

Cygwin tries to bring a POSIX-compatible environment to Windows so that most software that runs on unices will build and run on Cygwin without any significant modifications. … MSYS2 tries to provide an environment for building native Windows software. MSYS2 provides a large collection of packages containing such software, and libraries for their development.

So… Yeah. I was totally employing a tool not designed for my intended use just for its package manager (lol). I did encounter a few problems along the way—as expected, due to them being designed for different missions.

Note: This article covers some advanced topics. For basic usages of MSYS2, see this official introduction or this blog post by CadHut.

In the same wiki page mentioned above, we can see that the project intentionally disabled symlinks by default. This is fine if you really are just using MSYS2 for building native Windows software. I was planning to employ it as a general-purpose operating environment—thus needing to preserve symlinks in my dotfiles and various git repositories.

There are two possible solutions.

The first method is to just use Windows’s native symlink (see the “2020+ TL;DR Answer” section as for how to enable that). Once your Windows user gets mklink privilege (or you run everything with admin privilege—don’t do that), modify your MSYS2’s ini configuration and add this:

MSYS=winsymlinks:nativestrict

If you don’t want to meddle with these Windows things or your IT don’t want you to, there’s another way. Since MSYS2 is based on Cygwin, the Cygwin way still works—add this to the MSYS2’s ini configuration instead:

MSYS=winsymlinks:sys

Note that the links created with the second approach are standard Cygwin links—they’re just magic system files containing the destination. Therefore, non-Cygwin/non-MSYS2 (a.k.a. native Windows) programs can’t understand the symlink. This is fine for my use cases. It’s identical to the default Cygwin behavior.

ACL

In a similar vein, MSYS2 default to noacl intentionally. The Unix-style file permission system doesn’t play nicely with NTFS ACL control mechanism. Some native Windows programs will be allergic to files touched by Cygwin/MSYS2 ACL—for example, a dll or an executable with the executable bit unset will act rather funny.

Still, I wanted to preserve file permissions in git repositories. Adding this to /etc/fstab within MSYS2 enables ACL for the whole MSYS2 system, without affecting the rest of your Windows system:

C:\MSYS2 / ntfs override,acl,binary,posix=1 0 0

Of course, replace the C:\MSYS2 with the actual location of your installation.

Note: NTFS only. FAT partitions can’t do ACL.

Automatic Path Mangling

Automatic path mangling means when you call a native Windows executable from the MSYS2 shell, it will automatically convert Unix-style paths into Windows paths. For example, if you call a native ffmpeg from, say, /home/hojin, with the following command:

ffmpeg -i data/input.mkv ... /tmp/output.mkv

Then MSYS2 will implicitly convert the command line into something like:

ffmpeg -i C:\MSYS2\home\hojin\data\input.mkv ... C:\MSYS2\tmp\output.mkv

This is a pretty handy feature—I can drop most of my rickety wrapper scripts! But here’s the catch: MSYS2 doesn’t know which parameters are supposed to be paths. It’s just guessing. And there are indeed some ambiguous cases that will trigger the mechanism and cause strange results:

schtasks /Query /TN task_name

Here the /Query and /TN will be mistaken and converted to their Windows equivalents—and the Windows task scheduler will become very confused.

The solution lies in the official wiki—you can set an environment variable to prevent MSYS2 from doing so1:

export MSYS2_ARG_CONV_EXCL='*'
schtasks /Query /TN task_name

There’s an even wackier workaround—I forget where it came from: Double the slash in a parameter. The double slash acts as an escape mechanism, and that particular parameter won’t be interpreted as a path! For example:

schtasks //Query //TN task_name

This way, you can mix mangled parameters and non-mangled parameters in a single command line! I haven’t found any use cases for this weird workaround, though…

ACL-induced Issue for Pacman

After doing all the above, your pacman will start acting crazy, emitting a huge amount of warning messages saying it can’t change file ownership to uid=1 and gid=1. Without ACL, changing file ownership is a no-op inside MSYS22—I guess this is why MSYS2 devs decided to leave that part as is. With ACL enabled, this will be annoying.

There is no simple way around this. The easiest solution I can come up with is this: Grab the package specs from the MSYS2-packages source tree, add the following patch into PKGBUILD after all the MSYS2’s patches, then rebuild pacman with makepkg -csi:

diff -daur pacman-6.0.1/lib/libalpm/add.c pacman-6.0.1.new/lib/libalpm/add.c
--- pacman-6.0.1/lib/libalpm/add.c	2021-09-04 17:42:20.344308000 +0800
+++ pacman-6.0.1.new/lib/libalpm/add.c	2021-11-05 23:13:48.893874600 +0800
@@ -115,8 +115,7 @@
 {
 	int ret;
 	struct archive *archive_writer;
-	const int archive_flags = ARCHIVE_EXTRACT_OWNER |
-	                          ARCHIVE_EXTRACT_PERM |
+	const int archive_flags = ARCHIVE_EXTRACT_PERM |
 	                          ARCHIVE_EXTRACT_TIME |
 	                          ARCHIVE_EXTRACT_UNLINK |
 	                          ARCHIVE_EXTRACT_XATTR |

  1. I did exactly this in my CygwinSkipUAC script. ↩︎

  2. Really. You can try chmod and chown with ACL disabled to see what happens. ↩︎