From 27db4f893061c091c97f1e9cc6a8ba4e54e57f75 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 13 Mar 2015 14:16:43 +0100 Subject: [PATCH 1/5] Add Git for Windows' wrapper executable On Windows, Git is faced by the challenge that it has to set up certain environment variables before running Git under special circumstances such as when Git is called directly from cmd.exe (i.e. outside any Bash environment). This source code was taken from msysGit's commit 74a198d: https://github.com/msysgit/msysgit/blob/74a198d/src/git-wrapper/git-wrapper.c Signed-off-by: Johannes Schindelin --- compat/win32/git-wrapper.c | 194 +++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 compat/win32/git-wrapper.c diff --git a/compat/win32/git-wrapper.c b/compat/win32/git-wrapper.c new file mode 100644 index 0000000000..a7495c095f --- /dev/null +++ b/compat/win32/git-wrapper.c @@ -0,0 +1,194 @@ +/* + * git-wrapper - replace cmd\git.cmd with an executable + * + * Copyright (C) 2012 Pat Thoyts + */ + +#define STRICT +#define WIN32_LEAN_AND_MEAN +#define UNICODE +#define _UNICODE +#include +#include +#include +#include + +static void print_error(LPCWSTR prefix, DWORD error_number) +{ + LPWSTR buffer = NULL; + DWORD count = 0; + + count = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER + | FORMAT_MESSAGE_FROM_SYSTEM + | FORMAT_MESSAGE_IGNORE_INSERTS, + NULL, error_number, LANG_NEUTRAL, + (LPTSTR)&buffer, 0, NULL); + if (count < 1) + count = FormatMessageW(FORMAT_MESSAGE_ALLOCATE_BUFFER + | FORMAT_MESSAGE_FROM_STRING + | FORMAT_MESSAGE_ARGUMENT_ARRAY, + L"Code 0x%1!08x!", + 0, LANG_NEUTRAL, (LPTSTR)&buffer, 0, + (va_list*)&error_number); + fwprintf(stderr, L"%s: %s", prefix, buffer); + LocalFree((HLOCAL)buffer); +} + +int main(void) +{ + int r = 1, wait = 1; + WCHAR exepath[MAX_PATH], exe[MAX_PATH]; + LPWSTR cmd = NULL, path2 = NULL, exep = exe; + UINT codepage = 0; + int len; + + /* get the installation location */ + GetModuleFileName(NULL, exepath, MAX_PATH); + PathRemoveFileSpec(exepath); + PathRemoveFileSpec(exepath); + + /* set the default exe module */ + wcscpy(exe, exepath); + PathAppend(exe, L"bin\\git.exe"); + + /* if not set, set TERM to msys */ + if (!GetEnvironmentVariable(L"TERM", NULL, 0)) + SetEnvironmentVariable(L"TERM", L"msys"); + + /* if not set, set PLINK_PROTOCOL to ssh */ + if (!GetEnvironmentVariable(L"PLINK_PROTOCOL", NULL, 0)) + SetEnvironmentVariable(L"PLINK_PROTOCOL", L"ssh"); + + /* set HOME to %HOMEDRIVE%%HOMEPATH% or %USERPROFILE% + * With roaming profiles: HOMEPATH is the roaming location and + * USERPROFILE is the local location + */ + if (!GetEnvironmentVariable(L"HOME", NULL, 0)) { + LPWSTR e = NULL; + len = GetEnvironmentVariable(L"HOMEPATH", NULL, 0); + if (len == 0) { + len = GetEnvironmentVariable(L"USERPROFILE", NULL, 0); + if (len != 0) { + e = (LPWSTR)malloc(len * sizeof(WCHAR)); + GetEnvironmentVariable(L"USERPROFILE", e, len); + SetEnvironmentVariable(L"HOME", e); + free(e); + } + } + else { + int n; + len += GetEnvironmentVariable(L"HOMEDRIVE", NULL, 0); + e = (LPWSTR)malloc(sizeof(WCHAR) * (len + 2)); + n = GetEnvironmentVariable(L"HOMEDRIVE", e, len); + GetEnvironmentVariable(L"HOMEPATH", &e[n], len-n); + SetEnvironmentVariable(L"HOME", e); + free(e); + } + } + + /* extend the PATH */ + len = GetEnvironmentVariable(L"PATH", NULL, 0); + len = sizeof(WCHAR) * (len + 2 * MAX_PATH); + path2 = (LPWSTR)malloc(len); + wcscpy(path2, exepath); + PathAppend(path2, L"bin;"); + /* should do this only if it exists */ + wcscat(path2, exepath); + PathAppend(path2, L"mingw\\bin;"); + GetEnvironmentVariable(L"PATH", &path2[wcslen(path2)], + (len/sizeof(WCHAR))-wcslen(path2)); + SetEnvironmentVariable(L"PATH", path2); + free(path2); + + + /* fix up the command line to call git.exe + * We have to be very careful about quoting here so we just + * trim off the first argument and replace it leaving the rest + * untouched. + */ + { + int wargc = 0, gui = 0; + LPWSTR cmdline = NULL; + LPWSTR *wargv = NULL, p = NULL; + cmdline = GetCommandLine(); + wargv = CommandLineToArgvW(cmdline, &wargc); + cmd = (LPWSTR)malloc(sizeof(WCHAR) * + (wcslen(cmdline) + MAX_PATH)); + if (wargc > 1 && wcsicmp(L"gui", wargv[1]) == 0) { + wait = 0; + if (wargc > 2 && wcsicmp(L"citool", wargv[2]) == 0) { + wait = 1; + wcscpy(cmd, L"git.exe"); + } + else { + WCHAR script[MAX_PATH]; + gui = 1; + wcscpy(script, exepath); + PathAppend(script, + L"libexec\\git-core\\git-gui"); + PathQuoteSpaces(script); + wcscpy(cmd, L"wish.exe "); + wcscat(cmd, script); + wcscat(cmd, L" --"); + /* find the module from the commandline */ + exep = NULL; + } + } + else + wcscpy(cmd, L"git.exe"); + + /* append all after first space after the initial parameter */ + p = wcschr(&cmdline[wcslen(wargv[0])], L' '); + if (p && *p) { + /* for git gui subcommands, remove the 'gui' word */ + if (gui) { + while (*p == L' ') ++p; + p = wcschr(p, L' '); + } + if (p && *p) + wcscat(cmd, p); + } + LocalFree(wargv); + } + + /* set the console to ANSI/GUI codepage */ + codepage = GetConsoleCP(); + SetConsoleCP(GetACP()); + + { + STARTUPINFO si; + PROCESS_INFORMATION pi; + BOOL br = FALSE; + ZeroMemory(&pi, sizeof(PROCESS_INFORMATION)); + ZeroMemory(&si, sizeof(STARTUPINFO)); + si.cb = sizeof(STARTUPINFO); + br = CreateProcess(/* module: null means use command line */ + exep, + cmd, /* modified command line */ + NULL, /* process handle inheritance */ + NULL, /* thread handle inheritance */ + TRUE, /* handles inheritable? */ + CREATE_UNICODE_ENVIRONMENT, + NULL, /* environment: use parent */ + NULL, /* starting directory: use parent */ + &si, &pi); + if (br) { + if (wait) + WaitForSingleObject(pi.hProcess, INFINITE); + if (!GetExitCodeProcess(pi.hProcess, (DWORD *)&r)) + print_error(L"error reading exit code", + GetLastError()); + CloseHandle(pi.hProcess); + } + else { + print_error(L"error launching git", GetLastError()); + r = 1; + } + } + + free(cmd); + + /* reset the console codepage */ + SetConsoleCP(codepage); + ExitProcess(r); +} From aa086455e176f7996508adbf895946bd0f99105d Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 13 Mar 2015 15:36:41 +0100 Subject: [PATCH 2/5] mingw: Compile the Git wrapper We take care to embed the manifest, too, because we will modify the wrapper in the next few commits to serve as a drop-in replacement for the built-ins, i.e. we will want to call the wrapper under names such as 'git-patch-id.exe', too. Signed-off-by: Johannes Schindelin --- config.mak.uname | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config.mak.uname b/config.mak.uname index 00d0529ec7..41d0c1a8fe 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -561,6 +561,14 @@ else else NO_CURL = YesPlease endif + OTHER_PROGRAMS += git-wrapper$(X) + +git-wrapper$(X): compat/win32/git-wrapper.o git.res + $(QUIET_LINK)$(CC) -Wall -s -o $@ $^ -lshell32 -lshlwapi + +compat/win32/git-wrapper.o: %.o: %.c + $(QUIET_CC)$(CC) -o $*.o -c -Wall -Wwrite-strings $< + endif endif ifeq ($(uname_S),QNX) From b8e9daa455cfd1fe2e04440124b95eccc5b5db8b Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 13 Mar 2015 15:35:21 +0100 Subject: [PATCH 3/5] Refactor git-wrapper into more functions This prepares the wrapper for modifications to serve as a drop-in replacement for the builtins. This commit's diff is best viewed with the `-w` flag. Signed-off-by: Johannes Schindelin --- compat/win32/git-wrapper.c | 128 ++++++++++++++++++++----------------- 1 file changed, 68 insertions(+), 60 deletions(-) diff --git a/compat/win32/git-wrapper.c b/compat/win32/git-wrapper.c index a7495c095f..d4cfb563bb 100644 --- a/compat/win32/git-wrapper.c +++ b/compat/win32/git-wrapper.c @@ -34,22 +34,10 @@ static void print_error(LPCWSTR prefix, DWORD error_number) LocalFree((HLOCAL)buffer); } -int main(void) +static void setup_environment(LPWSTR exepath) { - int r = 1, wait = 1; - WCHAR exepath[MAX_PATH], exe[MAX_PATH]; - LPWSTR cmd = NULL, path2 = NULL, exep = exe; - UINT codepage = 0; int len; - - /* get the installation location */ - GetModuleFileName(NULL, exepath, MAX_PATH); - PathRemoveFileSpec(exepath); - PathRemoveFileSpec(exepath); - - /* set the default exe module */ - wcscpy(exe, exepath); - PathAppend(exe, L"bin\\git.exe"); + LPWSTR path2 = NULL; /* if not set, set TERM to msys */ if (!GetEnvironmentVariable(L"TERM", NULL, 0)) @@ -100,56 +88,76 @@ int main(void) SetEnvironmentVariable(L"PATH", path2); free(path2); +} - /* fix up the command line to call git.exe - * We have to be very careful about quoting here so we just - * trim off the first argument and replace it leaving the rest - * untouched. - */ - { - int wargc = 0, gui = 0; - LPWSTR cmdline = NULL; - LPWSTR *wargv = NULL, p = NULL; - cmdline = GetCommandLine(); - wargv = CommandLineToArgvW(cmdline, &wargc); - cmd = (LPWSTR)malloc(sizeof(WCHAR) * - (wcslen(cmdline) + MAX_PATH)); - if (wargc > 1 && wcsicmp(L"gui", wargv[1]) == 0) { - wait = 0; - if (wargc > 2 && wcsicmp(L"citool", wargv[2]) == 0) { - wait = 1; - wcscpy(cmd, L"git.exe"); - } - else { - WCHAR script[MAX_PATH]; - gui = 1; - wcscpy(script, exepath); - PathAppend(script, - L"libexec\\git-core\\git-gui"); - PathQuoteSpaces(script); - wcscpy(cmd, L"wish.exe "); - wcscat(cmd, script); - wcscat(cmd, L" --"); - /* find the module from the commandline */ - exep = NULL; - } - } - else +/* + * Fix up the command line to call git.exe + * We have to be very careful about quoting here so we just + * trim off the first argument and replace it leaving the rest + * untouched. + */ +static LPWSTR fixup_commandline(LPWSTR exepath, LPWSTR *exep, int *wait) +{ + int wargc = 0, gui = 0; + LPWSTR cmd = NULL, cmdline = NULL; + LPWSTR *wargv = NULL, p = NULL; + + cmdline = GetCommandLine(); + wargv = CommandLineToArgvW(cmdline, &wargc); + cmd = (LPWSTR)malloc(sizeof(WCHAR) * + (wcslen(cmdline) + MAX_PATH)); + if (wargc > 1 && wcsicmp(L"gui", wargv[1]) == 0) { + *wait = 0; + if (wargc > 2 && wcsicmp(L"citool", wargv[2]) == 0) { + *wait = 1; wcscpy(cmd, L"git.exe"); - - /* append all after first space after the initial parameter */ - p = wcschr(&cmdline[wcslen(wargv[0])], L' '); - if (p && *p) { - /* for git gui subcommands, remove the 'gui' word */ - if (gui) { - while (*p == L' ') ++p; - p = wcschr(p, L' '); - } - if (p && *p) - wcscat(cmd, p); } - LocalFree(wargv); + else { + WCHAR script[MAX_PATH]; + gui = 1; + wcscpy(script, exepath); + PathAppend(script, + L"libexec\\git-core\\git-gui"); + PathQuoteSpaces(script); + wcscpy(cmd, L"wish.exe "); + wcscat(cmd, script); + wcscat(cmd, L" --"); + /* find the module from the commandline */ + *exep = NULL; + } } + else + wcscpy(cmd, L"git.exe"); + + /* append all after first space after the initial parameter */ + p = wcschr(&cmdline[wcslen(wargv[0])], L' '); + if (p && *p) { + /* for git gui subcommands, remove the 'gui' word */ + if (gui) { + while (*p == L' ') ++p; + p = wcschr(p, L' '); + } + if (p && *p) + wcscat(cmd, p); + } + LocalFree(wargv); + + return cmd; +} + +int main(void) +{ + int r = 1, wait = 1; + WCHAR exepath[MAX_PATH], exe[MAX_PATH]; + LPWSTR cmd = NULL, exep = exe, basename; + UINT codepage = 0; + + /* get the installation location */ + GetModuleFileName(NULL, exepath, MAX_PATH); + PathRemoveFileSpec(exepath); + PathRemoveFileSpec(exepath); + setup_environment(exepath); + cmd = fixup_commandline(exepath, &exep, &wait); /* set the console to ANSI/GUI codepage */ codepage = GetConsoleCP(); From 7bfa170534341a02327ffb7393c15db11b24d3fe Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 13 Mar 2015 16:00:02 +0100 Subject: [PATCH 4/5] Let the Git wrapper serve as a drop-in replacement for builtins Git started out as a bunch of separate commands, in the true Unix spirit. Over time, more and more functionality was shared between the different Git commands, though, so it made sense to introduce the notion of "builtins": programs that are actually integrated into the main Git executable. These builtins can be called in two ways: either by specifying a subcommand as the first command-line argument, or -- for backwards compatibility -- by calling the Git executable hardlinked to a filename of the form "git-". Example: the "log" command can be called via "git log " or via "git-log ". The latter form is actually deprecated and only supported for scripts; calling "git-log" interactively will not even work by default because the libexec/git-core/ directory is not in the PATH. All of this is well and groovy as long as hard links are supported. Sadly, this is not the case in general on Windows. So it actually hurts quite a bit when you have to fall back to copying all of git.exe's currently 7.5MB 109 times, just for backwards compatibility. The simple solution would be to install really trivial shell script wrappers in place of the builtins: for builtin in $BUILTINS do rm git-$builtin.exe printf '#!/bin/sh\nexec git %s "$@"\n' $builtin > git-builtin chmod a+x git-builtin done This method would work -- even on Windows because Git for Windows ships a full-fledged Bash. However, the Windows Bash comes at a price: it needs to spin up a full-fledged POSIX emulation layer everytime it starts. Therefore, the shell script solution would incur a significant performance penalty. The best solution the Git for Windows team could come up with is to extend the Git wrapper -- that is needed to call Git from cmd.exe anyway, and that weighs in with a scant 19KB -- to also serve as a drop-in replacement for the builtins so that the following workaround is satisfactory: for builtin in $BUILTINS do cp git-wrapper.exe git-$builtin.exe done This commit allows for this, by extending the module file parsing to turn builtin command names like `git-log.exe ...` into calls to the main Git executable: `git.exe log ...`. Signed-off-by: Johannes Schindelin --- compat/win32/git-wrapper.c | 47 +++++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/compat/win32/git-wrapper.c b/compat/win32/git-wrapper.c index d4cfb563bb..0266617b15 100644 --- a/compat/win32/git-wrapper.c +++ b/compat/win32/git-wrapper.c @@ -96,7 +96,8 @@ static void setup_environment(LPWSTR exepath) * trim off the first argument and replace it leaving the rest * untouched. */ -static LPWSTR fixup_commandline(LPWSTR exepath, LPWSTR *exep, int *wait) +static LPWSTR fixup_commandline(LPWSTR exepath, LPWSTR *exep, int *wait, + LPWSTR builtin, int builtin_len) { int wargc = 0, gui = 0; LPWSTR cmd = NULL, cmdline = NULL; @@ -105,7 +106,7 @@ static LPWSTR fixup_commandline(LPWSTR exepath, LPWSTR *exep, int *wait) cmdline = GetCommandLine(); wargv = CommandLineToArgvW(cmdline, &wargc); cmd = (LPWSTR)malloc(sizeof(WCHAR) * - (wcslen(cmdline) + MAX_PATH)); + (wcslen(cmdline) + builtin_len + 1 + MAX_PATH)); if (wargc > 1 && wcsicmp(L"gui", wargv[1]) == 0) { *wait = 0; if (wargc > 2 && wcsicmp(L"citool", wargv[2]) == 0) { @@ -126,6 +127,9 @@ static LPWSTR fixup_commandline(LPWSTR exepath, LPWSTR *exep, int *wait) *exep = NULL; } } + else if (builtin) + _swprintf(cmd, L"%s\\%s %.*s", + exepath, L"git.exe", builtin_len, builtin); else wcscpy(cmd, L"git.exe"); @@ -147,17 +151,44 @@ static LPWSTR fixup_commandline(LPWSTR exepath, LPWSTR *exep, int *wait) int main(void) { - int r = 1, wait = 1; + int r = 1, wait = 1, builtin_len = -1; WCHAR exepath[MAX_PATH], exe[MAX_PATH]; - LPWSTR cmd = NULL, exep = exe, basename; + LPWSTR cmd = NULL, exep = exe, builtin = NULL, basename; UINT codepage = 0; /* get the installation location */ GetModuleFileName(NULL, exepath, MAX_PATH); - PathRemoveFileSpec(exepath); - PathRemoveFileSpec(exepath); - setup_environment(exepath); - cmd = fixup_commandline(exepath, &exep, &wait); + if (!PathRemoveFileSpec(exepath)) { + fwprintf(stderr, L"Invalid executable path: %s\n", exepath); + ExitProcess(1); + } + basename = exepath + wcslen(exepath) + 1; + if (!wcsncmp(basename, L"git-", 4)) { + /* Call a builtin */ + builtin = basename + 4; + builtin_len = wcslen(builtin); + if (!wcscmp(builtin + builtin_len - 4, L".exe")) + builtin_len -= 4; + + /* set the default exe module */ + wcscpy(exe, exepath); + PathAppend(exe, L"git.exe"); + } + else if (!wcscmp(basename, L"git.exe")) { + if (!PathRemoveFileSpec(exepath)) { + fwprintf(stderr, + L"Invalid executable path: %s\n", exepath); + ExitProcess(1); + } + + /* set the default exe module */ + wcscpy(exe, exepath); + PathAppend(exe, L"bin\\git.exe"); + } + + if (!builtin) + setup_environment(exepath); + cmd = fixup_commandline(exepath, &exep, &wait, builtin, builtin_len); /* set the console to ANSI/GUI codepage */ codepage = GetConsoleCP(); From 5727569a2c5ee3147596469c12036ea33deaf898 Mon Sep 17 00:00:00 2001 From: Johannes Schindelin Date: Fri, 13 Mar 2015 16:40:05 +0100 Subject: [PATCH 5/5] mingw: Use the Git wrapper for builtins This reduces the disk footprint of a full Git for Windows setup dramatically because on Windows, one cannot assume that hard links are supported. The net savings are calculated easily: the 32-bit `git.exe` file weighs in with 7662 kB while the `git-wrapper.exe` file (modified to serve as a drop-in replacement for builtins) weighs a scant 21 kB. At this point, there are 109 builtins which results in a total of 813 MB disk space being freed up by this commit. Yes, that is really more than half a gigabyte. Signed-off-by: Johannes Schindelin --- Makefile | 34 ++++++++++++++++++++++++++-------- config.mak.uname | 1 + 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index 20058f1de7..cc6bd7c32c 100644 --- a/Makefile +++ b/Makefile @@ -1658,11 +1658,17 @@ version.sp version.s version.o: EXTRA_CPPFLAGS = \ '-DGIT_VERSION="$(GIT_VERSION)"' \ '-DGIT_USER_AGENT=$(GIT_USER_AGENT_CQ_SQ)' +ifeq (,$(BUILT_IN_WRAPPER)) $(BUILT_INS): git$X $(QUIET_BUILT_IN)$(RM) $@ && \ ln $< $@ 2>/dev/null || \ ln -s $< $@ 2>/dev/null || \ cp $< $@ +else +$(BUILT_INS): $(BUILT_IN_WRAPPER) + $(QUIET_BUILT_IN)$(RM) $@ && \ + cp $< $@ +endif common-cmds.h: ./generate-cmdlist.sh command-list.txt @@ -2225,6 +2231,24 @@ profile-install: profile profile-fast-install: profile-fast $(MAKE) install +ifeq (,$(BUILT_IN_WRAPPER)) +LN_OR_CP_BUILT_IN_BINDIR = \ + test -z "$(NO_INSTALL_HARDLINKS)" && \ + ln "$$bindir/git$X" "$$bindir/$$p" 2>/dev/null || \ + ln -s "git$X" "$$bindir/$$p" 2>/dev/null || \ + cp "$$bindir/git$X" "$$bindir/$$p" || exit; +LN_OR_CP_BUILT_IN_EXECDIR = \ + test -z "$(NO_INSTALL_HARDLINKS)" && \ + ln "$$exectir/git$X" "$$exectir/$$p" 2>/dev/null || \ + ln -s "git$X" "$$exectir/$$p" 2>/dev/null || \ + cp "$$exectir/git$X" "$$exectir/$$p" || exit; +else +LN_OR_CP_BUILT_IN_BINDIR = \ + cp "$(BUILT_IN_WRAPPER)" "$$bindir/$$p" || exit; +LN_OR_CP_BUILT_IN_EXECDIR = \ + cp "$(BUILT_IN_WRAPPER)" "$$execdir/$$p" || exit; +endif + install: all $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(bindir_SQ)' $(INSTALL) -d -m 755 '$(DESTDIR_SQ)$(gitexec_instdir_SQ)' @@ -2263,17 +2287,11 @@ endif } && \ for p in $(filter $(install_bindir_programs),$(BUILT_INS)); do \ $(RM) "$$bindir/$$p" && \ - test -z "$(NO_INSTALL_HARDLINKS)" && \ - ln "$$bindir/git$X" "$$bindir/$$p" 2>/dev/null || \ - ln -s "git$X" "$$bindir/$$p" 2>/dev/null || \ - cp "$$bindir/git$X" "$$bindir/$$p" || exit; \ + $(LN_OR_CP_BUILT_IN_BINDIR) \ done && \ for p in $(BUILT_INS); do \ $(RM) "$$execdir/$$p" && \ - test -z "$(NO_INSTALL_HARDLINKS)" && \ - ln "$$execdir/git$X" "$$execdir/$$p" 2>/dev/null || \ - ln -s "git$X" "$$execdir/$$p" 2>/dev/null || \ - cp "$$execdir/git$X" "$$execdir/$$p" || exit; \ + $(LN_OR_CP_BUILT_IN_EXECDIR) \ done && \ remote_curl_aliases="$(REMOTE_CURL_ALIASES)" && \ for p in $$remote_curl_aliases; do \ diff --git a/config.mak.uname b/config.mak.uname index 41d0c1a8fe..2efcbe8884 100644 --- a/config.mak.uname +++ b/config.mak.uname @@ -562,6 +562,7 @@ else NO_CURL = YesPlease endif OTHER_PROGRAMS += git-wrapper$(X) + BUILT_IN_WRAPPER = git-wrapper$(X) git-wrapper$(X): compat/win32/git-wrapper.o git.res $(QUIET_LINK)$(CC) -Wall -s -o $@ $^ -lshell32 -lshlwapi