Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

execute git in WSL for WSL paths #532

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 2 additions & 2 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"args": ["--silent", "--config", "${workspaceFolder}/jest.fast.config.js"],
"console": "integratedTerminal",
"env": {
"LOCAL_GIT_DIRECTORY": "./git/"
"LOCAL_GIT_DIRECTORY": "./git/default"
},
"internalConsoleOptions": "neverOpen"
},
Expand All @@ -21,7 +21,7 @@
"args": ["--silent", "--config", "${workspaceFolder}/jest.slow.config.js"],
"console": "integratedTerminal",
"env": {
"LOCAL_GIT_DIRECTORY": "./git/"
"LOCAL_GIT_DIRECTORY": "./git/default"
},
"internalConsoleOptions": "neverOpen"
},
Expand Down
121 changes: 102 additions & 19 deletions lib/git-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,33 @@ function resolveEmbeddedGitDir(): string {
) {
const s = path.sep
return path
.resolve(__dirname, '..', '..', 'git')
.resolve(__dirname, '..', '..', 'git', 'default')
.replace(/[\\\/]app.asar[\\\/]/, `${s}app.asar.unpacked${s}`)
}
throw new Error('Git not supported on platform: ' + process.platform)
}

function resolveEmbeddedGitDirForWSL(): string {
return path
.resolve(__dirname, '..', '..', 'git_platforms', 'linux-x64')
.replace(/[\\\/]app.asar[\\\/]/, `${path.sep}app.asar.unpacked${path.sep}`)
}

/**
* Find the path to the embedded Git environment.
*
* If a custom Git directory path is defined as the `LOCAL_GIT_DIRECTORY` environment variable, then
* returns with it after resolving it as a path.
*/
function resolveGitDir(): string {
function resolveGitDir(wslDistro?: string): string {
if (wslDistro) {
if (process.env.LOCAL_GIT_DIRECTORY_WSL != null) {
return path.resolve(process.env.LOCAL_GIT_DIRECTORY_WSL)
} else {
return resolveEmbeddedGitDirForWSL()
}
}

if (process.env.LOCAL_GIT_DIRECTORY != null) {
return path.resolve(process.env.LOCAL_GIT_DIRECTORY)
} else {
Expand All @@ -32,12 +46,28 @@ function resolveGitDir(): string {
/**
* Find the path to the embedded Git binary.
*/
function resolveGitBinary(): string {
const gitDir = resolveGitDir()
function resolveGitBinary(wslDistro?: string): {
gitLocation: string
gitArgs: string[]
} {
const gitDir = resolveGitDir(wslDistro)

if (wslDistro) {
return {
gitLocation: 'wsl.exe',
gitArgs: [
'-d',
wslDistro,
'-e',
toWSLPath(path.join(gitDir, 'bin', 'git')),
],
}
}

if (process.platform === 'win32') {
return path.join(gitDir, 'cmd', 'git.exe')
return { gitLocation: path.join(gitDir, 'cmd', 'git.exe'), gitArgs: [] }
} else {
return path.join(gitDir, 'bin', 'git')
return { gitLocation: path.join(gitDir, 'bin', 'git'), gitArgs: [] }
}
}

Expand All @@ -47,7 +77,14 @@ function resolveGitBinary(): string {
* If a custom git exec path is given as the `GIT_EXEC_PATH` environment variable,
* then it returns with it after resolving it as a path.
*/
function resolveGitExecPath(): string {
function resolveGitExecPath(wslDistro?: string): string {
if (wslDistro) {
if (process.env.GIT_EXEC_PATH_WSL != null) {
return path.resolve(process.env.GIT_EXEC_PATH_WSL)
}
return path.join(resolveGitDir(wslDistro), 'libexec', 'git-core')
}

if (process.env.GIT_EXEC_PATH != null) {
return path.resolve(process.env.GIT_EXEC_PATH)
}
Expand All @@ -63,6 +100,19 @@ function resolveGitExecPath(): string {
}
}

/**
* Convert a windows path to a WSL path.
*/
export function toWSLPath(windowsPath: string) {
const [, drive, path] = windowsPath.match(/^([a-zA-Z]):(.*)/) ?? []

if (!drive || !path) {
return windowsPath
}

return `/mnt/${drive.toLowerCase()}${path.replace(/\\/g, '/')}`
}

/**
* Setup the process environment before invoking Git.
*
Expand All @@ -71,16 +121,23 @@ function resolveGitExecPath(): string {
*
* @param additional options to include with the process
*/
export function setupEnvironment(environmentVariables: NodeJS.ProcessEnv): {
export function setupEnvironment(
environmentVariables: NodeJS.ProcessEnv,
targetPath: string
): {
env: NodeJS.ProcessEnv
gitLocation: string
gitArgs: string[]
} {
const gitLocation = resolveGitBinary()
const [, wslDistro] =
targetPath.match(/\\\\wsl(?:\$|\.localhost)\\([^\\]+)\\/) ?? []

const { gitLocation, gitArgs } = resolveGitBinary(wslDistro)

let envPath: string = process.env.PATH || ''
const gitDir = resolveGitDir()
const gitDir = resolveGitDir(wslDistro)

if (process.platform === 'win32') {
if (process.platform === 'win32' && !wslDistro) {
if (process.arch === 'x64') {
envPath = `${gitDir}\\mingw64\\bin;${gitDir}\\mingw64\\usr\\bin;${envPath}`
} else {
Expand All @@ -92,12 +149,31 @@ export function setupEnvironment(environmentVariables: NodeJS.ProcessEnv): {
{},
process.env,
{
GIT_EXEC_PATH: resolveGitExecPath(),
GIT_EXEC_PATH: resolveGitExecPath(wslDistro),
PATH: envPath,
},
environmentVariables
)

if (wslDistro) {
// Forward certain environment variables to WSL to allow authentication.
// The /p flag translates windows<->wsl paths.
env.WSLENV = [
env?.WSLENV,
'GIT_ASKPASS/p',
'DESKTOP_USERNAME',
'DESKTOP_ENDPOINT',
'DESKTOP_PORT',
'DESKTOP_TRAMPOLINE_TOKEN',
'DESKTOP_TRAMPOLINE_IDENTIFIER',
'GIT_EXEC_PATH/p',
'GIT_TEMPLATE_DIR/p',
'PREFIX/p',
]
.filter(Boolean)
.join(':')
}

if (process.platform === 'win32') {
// while reading the environment variable is case-insensitive
// you can create a hash with multiple values, which means the
Expand All @@ -109,27 +185,34 @@ export function setupEnvironment(environmentVariables: NodeJS.ProcessEnv): {
}
}

if (process.platform === 'darwin' || process.platform === 'linux') {
if (
process.platform === 'darwin' ||
process.platform === 'linux' ||
wslDistro
) {
// templates are used to populate your .git folder
// when a repository is initialized locally
const templateDir = `${gitDir}/share/git-core/templates`
const templateDir = path.join(gitDir, 'share/git-core/templates')
env.GIT_TEMPLATE_DIR = templateDir
}

if (process.platform === 'linux') {
if (process.platform === 'linux' || wslDistro) {
// when building Git for Linux and then running it from
// an arbitrary location, you should set PREFIX for the
// process to ensure that it knows how to resolve things
env.PREFIX = gitDir

if (!env.GIT_SSL_CAINFO && !env.LOCAL_GIT_DIRECTORY) {
if (
!env.GIT_SSL_CAINFO &&
!(wslDistro ? env.LOCAL_GIT_DIRECTORY_WSL : env.LOCAL_GIT_DIRECTORY)
) {
// use the SSL certificate bundle included in the distribution only
// when using embedded Git and not providing your own bundle
const distDir = resolveEmbeddedGitDir()
const sslCABundle = `${distDir}/ssl/cacert.pem`
const distDir = resolveGitDir(wslDistro)
const sslCABundle = path.join(distDir, 'ssl/cacert.pem')
env.GIT_SSL_CAINFO = sslCABundle
}
}

return { env, gitLocation }
return { env, gitLocation, gitArgs }
}
8 changes: 4 additions & 4 deletions lib/git-process.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,14 +116,14 @@ export class GitProcess {
customEnv = options.env
}

const { env, gitLocation } = setupEnvironment(customEnv)
const { env, gitLocation, gitArgs } = setupEnvironment(customEnv, path)

const spawnArgs = {
env,
cwd: path,
}

const spawnedProcess = spawn(gitLocation, args, spawnArgs)
const spawnedProcess = spawn(gitLocation, [...gitArgs, ...args], spawnArgs)

ignoreClosedInputStream(spawnedProcess)

Expand Down Expand Up @@ -183,7 +183,7 @@ export class GitProcess {
customEnv = options.env
}

const { env, gitLocation } = setupEnvironment(customEnv)
const { env, gitLocation, gitArgs } = setupEnvironment(customEnv, path)

// Explicitly annotate opts since typescript is unable to infer the correct
// signature for execFile when options is passed as an opaque hash. The type
Expand All @@ -199,7 +199,7 @@ export class GitProcess {

const spawnedProcess = execFile(
gitLocation,
args,
[...gitArgs, ...args],
execOptions,
function (err: Error | null, stdout, stderr) {
result.updateProcessEnded()
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@
"prepack": "yarn build && yarn test",
"postpublish": "git push --follow-tags",
"test": "yarn test:fast && yarn test:slow && yarn test:external",
"test:fast": "cross-env LOCAL_GIT_DIRECTORY=./git/ jest --runInBand --silent --config ./jest.fast.config.js",
"test:slow": "cross-env LOCAL_GIT_DIRECTORY=./git/ jest --runInBand --silent --config ./jest.slow.config.js",
"test:fast": "cross-env LOCAL_GIT_DIRECTORY=./git/default jest --runInBand --silent --config ./jest.fast.config.js",
"test:slow": "cross-env LOCAL_GIT_DIRECTORY=./git/default jest --runInBand --silent --config ./jest.slow.config.js",
"test:external": "jest --runInBand --silent --config ./jest.external.config.js",
"download-git": "node ./script/download-git.js",
"postinstall": "node ./script/download-git.js",
Expand Down
66 changes: 44 additions & 22 deletions script/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,26 @@ const embeddedGit = require('./embedded-git.json')

function getConfig() {
const config = {
outputPath: path.join(__dirname, '..', 'git'),
outputPath: path.join(__dirname, '..', 'git', 'default'),
source: '',
checksum: '',
fileName: '',
tempFile: ''
tempFile: '',
}

// Possible values are ‘x64’, ‘arm’, ‘arm64’, ‘s390’, ‘s390x’, ‘mipsel’, ‘ia32’, ‘mips’, ‘ppc’ and ‘ppc64’
let arch = os.arch();
let arch = os.arch()

if (process.env.npm_config_arch) {
// If a specific npm_config_arch is set, we use that one instead of the OS arch (to support cross compilation)
console.log('npm_config_arch detected: ' + process.env.npm_config_arch);
arch = process.env.npm_config_arch;
console.log('npm_config_arch detected: ' + process.env.npm_config_arch)
arch = process.env.npm_config_arch
}

if (process.platform === 'win32' && arch === 'arm64') {
// Use the Dugite Native ia32 package for Windows arm64 (arm64 can run 32-bit code through emulation)
console.log('Downloading 32-bit Dugite Native for Windows arm64');
arch = 'ia32';
console.log('Downloading 32-bit Dugite Native for Windows arm64')
arch = 'ia32'
}

const key = `${process.platform}-${arch}`
Expand All @@ -37,30 +37,52 @@ function getConfig() {
config.checksum = entry.checksum
config.source = entry.url
} else {
console.log(`No embedded Git found for ${process.platform} and architecture ${arch}`)
console.log(
`No embedded Git found for ${process.platform} and architecture ${arch}`
)
}

if (config.source !== '') {
// compute the filename from the download source
const url = URL.parse(config.source)
const pathName = url.pathname
const index = pathName.lastIndexOf('/')
config.fileName = pathName.substr(index + 1)

const cacheDirEnv = process.env.DUGITE_CACHE_DIR
processConfig(config)
}

const cacheDir = cacheDirEnv ? path.resolve(cacheDirEnv) : os.tmpdir()
if (process.platform === 'win32') {
const entry = embeddedGit['linux-x64']

try {
fs.statSync(cacheDir)
} catch (e) {
fs.mkdirSync(cacheDir)
const wslConfig = {
outputPath: path.join(__dirname, '..', 'git', 'linux-x64'),
source: entry.url,
checksum: entry.checksum,
fileName: '',
tempFile: '',
}

config.tempFile = path.join(cacheDir, config.fileName)
processConfig(wslConfig)

return [config, wslConfig]
}

return [config]
}

function processConfig(config) {
// compute the filename from the download source
const url = URL.parse(config.source)
const pathName = url.pathname
const index = pathName.lastIndexOf('/')
config.fileName = pathName.substr(index + 1)

const cacheDirEnv = process.env.DUGITE_CACHE_DIR

const cacheDir = cacheDirEnv ? path.resolve(cacheDirEnv) : os.tmpdir()

try {
fs.statSync(cacheDir)
} catch (e) {
fs.mkdirSync(cacheDir)
}

return config
config.tempFile = path.join(cacheDir, config.fileName)
}

module.exports = getConfig