Skip to content

Instantly share code, notes, and snippets.

@jimklimov
Created April 1, 2023 12:23
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save jimklimov/291b7ee632a729e9412cb823c69c531a to your computer and use it in GitHub Desktop.
Save jimklimov/291b7ee632a729e9412cb823c69c531a to your computer and use it in GitHub Desktop.
WSL2 wrapper for `git difftool`
# Add these lines to your ~/.gitconfig, to use wsl_wrapper
# program and your helper script to call winmerge GUI:
[mergetool "winmerge"]
cmd = \"$HOME/bin/winmerge.sh\" -u -maximize -wl -wr -fm -dl 'Mine: '\"$LOCAL\" -dm 'Merged: '\"$BASE\" -dr 'Theirs: '\"$REMOTE\" \"$LOCAL\" \"$BASE\" \"$REMOTE\" -o \"$MERGED\" -am
trustExitCode = true
[difftool "winmerge"]
cmd = \"$HOME/bin/winmerge.sh\" \"$LOCAL\" \"$REMOTE\"
[diff]
tool = winmerge
[merge]
tool = winmerge
# Copyright (C) 2023 by Jim Klimov, licensed under terms of MIT license
all: wsl_wrapper
wsl_wrapper: wsl_wrapper.cpp
g++ "$<" -o "$@"
install: wsl_wrapper
mkdir -p "$HOME/bin"
cp -pf wsl_wrapper "$HOME/bin/"
chmod +x "$HOME/bin/wsl_wrapper"
#!/bin/sh
# Copyright (C) 2023 by Jim Klimov, licensed under terms of MIT license
# Example script to wrap WinMergeU.exe with wsl_wrapper as a "git difftool"
# install as $HOME/bin/winmerge.sh
# Of course can wrap another preferred tool similarly
# The symlink name of "wsl_wrapper" must be same as your Windows tool without the ".exe"
PATH="$HOME/bin:/c/Program Files/WinMerge:$PATH"
export PATH
if [ -s "$HOME/bin/wsl_wrapper" ] && [ ! -e "$HOME/bin/WinMergeU" ] ; then
ln -fs wsl_wrapper "$HOME/bin/WinMergeU"
fi
WinMergeU "$@"
/* Derived from https://stackoverflow.com/a/74977336/4715872
* by https://stackoverflow.com/users/40756/die-in-sente
* Slight fixes 2023 by Jim Klimov
* Build: g++ wsl_wrapper.cpp -o "$HOME/bin/wsl_wrapper"
*/
// Problem:
// It's difficult to launch a Windows GUI program from WSL.
// Take p4merge for example.
// I have the Windows version of p4merge installed, and I would
// like to set that as my git difftool, for comparing versions
// of source files.
// If I install it as my git difftool
// git config --global --add diff.tool p4merge
// Git will try to launch it, but it won't work because the
// file paths passed as part of the command are WSL paths,
// that the Winodws app can't open.
// p4merge will fail with an error, e.g.:
// '/tmp/vT1r3J_foo.cpp' is (or points to) an invalid file.
//
// Solution:
// This helper program can be installed on the WSL Ubuntu
// $PATH as p4merge. Or you can create a symlink in your
// $PATH named p4merge that points to this program.
//
// This will scan all of the command line arguemments and translate
// any file paths to paths that Windows apps can open, then
// launch the target with the modified command line.
//
// You can use this wrapper around any windows GUI program.
// It will figure out what target to launch from argv[0].
// HOWEVER! If this wrapper is in the $PATH and the target
// is also in the $PATH, they CANNOT HAVE THE SAME NAME!
// That would cause an infinite recursion where the wrapper
// kept launching itself.
// Recommended usage: Have "program" (without extension) in
// your path pointing to the wrapper, and "program.exe" in
// your path pointing to the Windows GUI target.
// This wrapper will automatically append the ".exe" suffix
// to the name in argv[0], and will quit without launching
// if argv[0] already contains an ".exe" extension.
//
// Note: When the linux shell launches a program via a symlink,
// argv[0] will have the name of the link, not the link target.
//
// Any argument on the command line that looks like a file or
// path will be translated if necessary. A path that is
// already in Windows format will just be copied.
//
// The WSL file system is not directly part of your computer's
// Windows namespace. The WSL file system is accessed as if
// it was a network file.
// A path that starts with "/" -- the WSL root directory translates
// to \\wsl$\${WSL_DISTRO_NAME}\
// A path that starts with "~" -- translates to
// \\wsl$\${WSL_DISTRO_NAME}\${HOME}
// And of course paths that start with "." translate to
// \\wsl$\${WSL_DISTRO_NAME}\${PWD}
//
// The Windows file system is accessable from linux as mounted
// devices. A path like /mnt/<drive-letter>/... translates to
// <drive-letter>:\...
//
// All forward-slashes are translated to back-slashes.
//
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <ctype.h>
#include <string>
#include <sstream>
// This really oughta be in libc
//
char * stristr (const char * text, const char * match)
{
char * strText = strdup (text);
for (char * p = strText; !!(*p); ++p) {
*p = tolower (*p);
}
char * strMatch = strdup (match);
for (char * p = strMatch; !!(*p); ++p) {
*p = tolower (*p);
}
char * result = strstr (strText, strMatch);
if (result) {
result = (char *) text + (result - strText);
}
free (strText);
free (strMatch);
return result;
}
int main (int argc, char ** argv) {
for (int i = 0; i < argc; ++i) {
fprintf (stdout, "arg[%2d] : %s\n", i, argv[i]);
}
fflush (stdout);
struct _local {
const char * env_WSL_DISTRO_NAME;
const char * env_HOME;
const char * env_PWD;
char * target;
std::stringstream command;
_local()
: env_WSL_DISTRO_NAME (nullptr)
, env_HOME (nullptr)
, env_PWD (nullptr)
, target (nullptr)
{}
~_local() {
if (target) {
auto ptr = target;
target = nullptr;
free (ptr);
}
}
static void get_env (const char * & member, const char * name) {
member = getenv (name);
fprintf (stdout, "%s=%s\n", (const char *) name,
member ? member : "");
}
void get_target (const char * me) {
const char * tmp = basename (me);
size_t n = strlen (tmp) + 5;
n = (n + 32) & 0x1f;
target = (char *) malloc (n);
memset (target, 0, n);
--n;
strncpy (target, tmp, n);
n -= strlen (target);
strncat (target, ".exe", n);
command << target;
}
void translate (const char * arg) {
if (! arg) return;
char c = *(arg++);
if ('~' == c) {
translate (env_HOME);
} else if ('.' == c && ('\0' == *arg || '\\' == *arg || '/' == *arg)) {
translate (env_PWD);
++arg;
} else if ('/' == c) {
if (0 == strncmp ("mnt/", arg, 4)) {
c = toupper (*(arg += 4));
command << c;
command << ":";
arg+=2;
} else {
command << "\\\\\\\\wsl$\\\\";
command << env_WSL_DISTRO_NAME;
}
}
for (c = arg[-1]; !!c; c = *(arg++)) {
if ('/' == c) {
command << "\\\\";
} else {
command << c;
}
}
}
void do_arg (const char * arg) {
command << ' ';
translate (arg);
}
} local;
local.get_env (local.env_WSL_DISTRO_NAME, "WSL_DISTRO_NAME");
local.get_env (local.env_HOME, "HOME");
local.get_env (local.env_PWD, "PWD");
if (stristr (argv[0], ".exe")) {
fprintf (stderr, "argv[0] = \"%s\"\n", (const char *) argv[0]);
fputs ("The wsl_wrapper has the same name as the implied target.\n"
"This would trigger infinite launch recursion!\n", stderr);
return 1;
}
local.get_target (argv[0]);
for (int i = 1; i < argc; ++i) {
local.do_arg (argv[i]);
}
fputs ("\033[1;33m", stdout); // YELLOW
fputs (local.command.str().c_str(), stdout);
fputs ("\033[0m\n", stdout);
fflush (stdout);
int result = system (local.command.str().c_str());
if (result == -1) {
fputs ("\033[1;31m", stdout); // RED
perror ("Failed to launch:");
fputs ("\033[0m\n", stdout);
}
// fputc ('\n', stdout);
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment