mirror of
				https://github.com/actions/cache.git
				synced 2025-10-31 18:34:19 +08:00 
			
		
		
		
	Merge pull request #252 from actions/users/aiyan/fallback-to-gnu-tar
Fallback to GNU tar if BSD tar is unavailable on windows machine
This commit is contained in:
		
						commit
						12b87469d4
					
				| @ -1,11 +1,12 @@ | |||||||
| import * as exec from "@actions/exec"; | import * as exec from "@actions/exec"; | ||||||
| import * as io from "@actions/io"; | import * as io from "@actions/io"; | ||||||
| import { promises as fs } from "fs"; |  | ||||||
| import * as path from "path"; | import * as path from "path"; | ||||||
| 
 | 
 | ||||||
| import { CacheFilename } from "../src/constants"; | import { CacheFilename } from "../src/constants"; | ||||||
| import * as tar from "../src/tar"; | import * as tar from "../src/tar"; | ||||||
| 
 | 
 | ||||||
|  | import fs = require("fs"); | ||||||
|  | 
 | ||||||
| jest.mock("@actions/exec"); | jest.mock("@actions/exec"); | ||||||
| jest.mock("@actions/io"); | jest.mock("@actions/io"); | ||||||
| 
 | 
 | ||||||
| @ -27,37 +28,75 @@ afterAll(async () => { | |||||||
|     await jest.requireActual("@actions/io").rmRF(getTempDir()); |     await jest.requireActual("@actions/io").rmRF(getTempDir()); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test("extract tar", async () => { | test("extract BSD tar", async () => { | ||||||
|     const mkdirMock = jest.spyOn(io, "mkdirP"); |     const mkdirMock = jest.spyOn(io, "mkdirP"); | ||||||
|     const execMock = jest.spyOn(exec, "exec"); |     const execMock = jest.spyOn(exec, "exec"); | ||||||
| 
 | 
 | ||||||
|     const archivePath = "cache.tar"; |     const IS_WINDOWS = process.platform === "win32"; | ||||||
|  |     const archivePath = IS_WINDOWS | ||||||
|  |         ? `${process.env["windir"]}\\fakepath\\cache.tar` | ||||||
|  |         : "cache.tar"; | ||||||
|     const workspace = process.env["GITHUB_WORKSPACE"]; |     const workspace = process.env["GITHUB_WORKSPACE"]; | ||||||
| 
 | 
 | ||||||
|     await tar.extractTar(archivePath); |     await tar.extractTar(archivePath); | ||||||
| 
 | 
 | ||||||
|     expect(mkdirMock).toHaveBeenCalledWith(workspace); |     expect(mkdirMock).toHaveBeenCalledWith(workspace); | ||||||
| 
 | 
 | ||||||
|     const IS_WINDOWS = process.platform === "win32"; |  | ||||||
|     const tarPath = IS_WINDOWS |     const tarPath = IS_WINDOWS | ||||||
|         ? `${process.env["windir"]}\\System32\\tar.exe` |         ? `${process.env["windir"]}\\System32\\tar.exe` | ||||||
|         : "tar"; |         : "tar"; | ||||||
|     expect(execMock).toHaveBeenCalledTimes(1); |     expect(execMock).toHaveBeenCalledTimes(1); | ||||||
|     expect(execMock).toHaveBeenCalledWith( |     expect(execMock).toHaveBeenCalledWith( | ||||||
|         `"${tarPath}"`, |         `"${tarPath}"`, | ||||||
|         ["-xz", "-f", archivePath, "-P", "-C", workspace], |         [ | ||||||
|  |             "-xz", | ||||||
|  |             "-f", | ||||||
|  |             IS_WINDOWS ? archivePath.replace(/\\/g, "/") : archivePath, | ||||||
|  |             "-P", | ||||||
|  |             "-C", | ||||||
|  |             IS_WINDOWS ? workspace?.replace(/\\/g, "/") : workspace | ||||||
|  |         ], | ||||||
|         { cwd: undefined } |         { cwd: undefined } | ||||||
|     ); |     ); | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
| test("create tar", async () => { | test("extract GNU tar", async () => { | ||||||
|  |     const IS_WINDOWS = process.platform === "win32"; | ||||||
|  |     if (IS_WINDOWS) { | ||||||
|  |         jest.spyOn(fs, "existsSync").mockReturnValueOnce(false); | ||||||
|  |         jest.spyOn(tar, "isGnuTar").mockReturnValue(Promise.resolve(true)); | ||||||
|  | 
 | ||||||
|  |         const execMock = jest.spyOn(exec, "exec"); | ||||||
|  |         const archivePath = `${process.env["windir"]}\\fakepath\\cache.tar`; | ||||||
|  |         const workspace = process.env["GITHUB_WORKSPACE"]; | ||||||
|  | 
 | ||||||
|  |         await tar.extractTar(archivePath); | ||||||
|  | 
 | ||||||
|  |         expect(execMock).toHaveBeenCalledTimes(2); | ||||||
|  |         expect(execMock).toHaveBeenLastCalledWith( | ||||||
|  |             `"tar"`, | ||||||
|  |             [ | ||||||
|  |                 "-xz", | ||||||
|  |                 "-f", | ||||||
|  |                 archivePath.replace(/\\/g, "/"), | ||||||
|  |                 "-P", | ||||||
|  |                 "-C", | ||||||
|  |                 workspace?.replace(/\\/g, "/"), | ||||||
|  |                 "--force-local" | ||||||
|  |             ], | ||||||
|  |             { cwd: undefined } | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | test("create BSD tar", async () => { | ||||||
|     const execMock = jest.spyOn(exec, "exec"); |     const execMock = jest.spyOn(exec, "exec"); | ||||||
| 
 | 
 | ||||||
|     const archiveFolder = getTempDir(); |     const archiveFolder = getTempDir(); | ||||||
|     const workspace = process.env["GITHUB_WORKSPACE"]; |     const workspace = process.env["GITHUB_WORKSPACE"]; | ||||||
|     const sourceDirectories = ["~/.npm/cache", `${workspace}/dist`]; |     const sourceDirectories = ["~/.npm/cache", `${workspace}/dist`]; | ||||||
| 
 | 
 | ||||||
|     await fs.mkdir(archiveFolder, { recursive: true }); |     await fs.promises.mkdir(archiveFolder, { recursive: true }); | ||||||
| 
 | 
 | ||||||
|     await tar.createTar(archiveFolder, sourceDirectories); |     await tar.createTar(archiveFolder, sourceDirectories); | ||||||
| 
 | 
 | ||||||
| @ -72,10 +111,10 @@ test("create tar", async () => { | |||||||
|         [ |         [ | ||||||
|             "-cz", |             "-cz", | ||||||
|             "-f", |             "-f", | ||||||
|             CacheFilename, |             IS_WINDOWS ? CacheFilename.replace(/\\/g, "/") : CacheFilename, | ||||||
|             "-P", |             "-P", | ||||||
|             "-C", |             "-C", | ||||||
|             workspace, |             IS_WINDOWS ? workspace?.replace(/\\/g, "/") : workspace, | ||||||
|             "--files-from", |             "--files-from", | ||||||
|             "manifest.txt" |             "manifest.txt" | ||||||
|         ], |         ], | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								dist/restore/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										46
									
								
								dist/restore/index.js
									
									
									
									
										vendored
									
									
								
							| @ -4959,12 +4959,30 @@ var __importStar = (this && this.__importStar) || function (mod) { | |||||||
|     return result; |     return result; | ||||||
| }; | }; | ||||||
| Object.defineProperty(exports, "__esModule", { value: true }); | Object.defineProperty(exports, "__esModule", { value: true }); | ||||||
|  | const core = __importStar(__webpack_require__(470)); | ||||||
| const exec_1 = __webpack_require__(986); | const exec_1 = __webpack_require__(986); | ||||||
| const io = __importStar(__webpack_require__(1)); | const io = __importStar(__webpack_require__(1)); | ||||||
| const fs_1 = __webpack_require__(747); | const fs_1 = __webpack_require__(747); | ||||||
| const path = __importStar(__webpack_require__(622)); | const path = __importStar(__webpack_require__(622)); | ||||||
| const constants_1 = __webpack_require__(694); | const constants_1 = __webpack_require__(694); | ||||||
| function getTarPath() { | function isGnuTar() { | ||||||
|  |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |         core.debug("Checking tar --version"); | ||||||
|  |         let versionOutput = ""; | ||||||
|  |         yield exec_1.exec("tar --version", [], { | ||||||
|  |             ignoreReturnCode: true, | ||||||
|  |             silent: true, | ||||||
|  |             listeners: { | ||||||
|  |                 stdout: (data) => (versionOutput += data.toString()), | ||||||
|  |                 stderr: (data) => (versionOutput += data.toString()) | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         core.debug(versionOutput.trim()); | ||||||
|  |         return versionOutput.toUpperCase().includes("GNU TAR"); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | exports.isGnuTar = isGnuTar; | ||||||
|  | function getTarPath(args) { | ||||||
|     return __awaiter(this, void 0, void 0, function* () { |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|         // Explicitly use BSD Tar on Windows
 |         // Explicitly use BSD Tar on Windows
 | ||||||
|         const IS_WINDOWS = process.platform === "win32"; |         const IS_WINDOWS = process.platform === "win32"; | ||||||
| @ -4973,22 +4991,21 @@ function getTarPath() { | |||||||
|             if (fs_1.existsSync(systemTar)) { |             if (fs_1.existsSync(systemTar)) { | ||||||
|                 return systemTar; |                 return systemTar; | ||||||
|             } |             } | ||||||
|  |             else if (isGnuTar()) { | ||||||
|  |                 args.push("--force-local"); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         return yield io.which("tar", true); |         return yield io.which("tar", true); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| function execTar(args, cwd) { | function execTar(args, cwd) { | ||||||
|     var _a, _b; |     var _a; | ||||||
|     return __awaiter(this, void 0, void 0, function* () { |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|         try { |         try { | ||||||
|             yield exec_1.exec(`"${yield getTarPath()}"`, args, { cwd: cwd }); |             yield exec_1.exec(`"${yield getTarPath(args)}"`, args, { cwd: cwd }); | ||||||
|         } |         } | ||||||
|         catch (error) { |         catch (error) { | ||||||
|             const IS_WINDOWS = process.platform === "win32"; |             throw new Error(`Tar failed with error: ${(_a = error) === null || _a === void 0 ? void 0 : _a.message}`); | ||||||
|             if (IS_WINDOWS) { |  | ||||||
|                 throw new Error(`Tar failed with error: ${(_a = error) === null || _a === void 0 ? void 0 : _a.message}. Ensure BSD tar is installed and on the PATH.`); |  | ||||||
|             } |  | ||||||
|             throw new Error(`Tar failed with error: ${(_b = error) === null || _b === void 0 ? void 0 : _b.message}`); |  | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| @ -5001,7 +5018,14 @@ function extractTar(archivePath) { | |||||||
|         // Create directory to extract tar into
 |         // Create directory to extract tar into
 | ||||||
|         const workingDirectory = getWorkingDirectory(); |         const workingDirectory = getWorkingDirectory(); | ||||||
|         yield io.mkdirP(workingDirectory); |         yield io.mkdirP(workingDirectory); | ||||||
|         const args = ["-xz", "-f", archivePath, "-P", "-C", workingDirectory]; |         const args = [ | ||||||
|  |             "-xz", | ||||||
|  |             "-f", | ||||||
|  |             archivePath.replace(new RegExp("\\" + path.sep, "g"), "/"), | ||||||
|  |             "-P", | ||||||
|  |             "-C", | ||||||
|  |             workingDirectory.replace(new RegExp("\\" + path.sep, "g"), "/") | ||||||
|  |         ]; | ||||||
|         yield execTar(args); |         yield execTar(args); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| @ -5015,10 +5039,10 @@ function createTar(archiveFolder, sourceDirectories) { | |||||||
|         const args = [ |         const args = [ | ||||||
|             "-cz", |             "-cz", | ||||||
|             "-f", |             "-f", | ||||||
|             constants_1.CacheFilename, |             constants_1.CacheFilename.replace(new RegExp("\\" + path.sep, "g"), "/"), | ||||||
|             "-P", |             "-P", | ||||||
|             "-C", |             "-C", | ||||||
|             workingDirectory, |             workingDirectory.replace(new RegExp("\\" + path.sep, "g"), "/"), | ||||||
|             "--files-from", |             "--files-from", | ||||||
|             manifestFilename |             manifestFilename | ||||||
|         ]; |         ]; | ||||||
|  | |||||||
							
								
								
									
										46
									
								
								dist/save/index.js
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										46
									
								
								dist/save/index.js
									
									
									
									
										vendored
									
									
								
							| @ -4936,12 +4936,30 @@ var __importStar = (this && this.__importStar) || function (mod) { | |||||||
|     return result; |     return result; | ||||||
| }; | }; | ||||||
| Object.defineProperty(exports, "__esModule", { value: true }); | Object.defineProperty(exports, "__esModule", { value: true }); | ||||||
|  | const core = __importStar(__webpack_require__(470)); | ||||||
| const exec_1 = __webpack_require__(986); | const exec_1 = __webpack_require__(986); | ||||||
| const io = __importStar(__webpack_require__(1)); | const io = __importStar(__webpack_require__(1)); | ||||||
| const fs_1 = __webpack_require__(747); | const fs_1 = __webpack_require__(747); | ||||||
| const path = __importStar(__webpack_require__(622)); | const path = __importStar(__webpack_require__(622)); | ||||||
| const constants_1 = __webpack_require__(694); | const constants_1 = __webpack_require__(694); | ||||||
| function getTarPath() { | function isGnuTar() { | ||||||
|  |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|  |         core.debug("Checking tar --version"); | ||||||
|  |         let versionOutput = ""; | ||||||
|  |         yield exec_1.exec("tar --version", [], { | ||||||
|  |             ignoreReturnCode: true, | ||||||
|  |             silent: true, | ||||||
|  |             listeners: { | ||||||
|  |                 stdout: (data) => (versionOutput += data.toString()), | ||||||
|  |                 stderr: (data) => (versionOutput += data.toString()) | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  |         core.debug(versionOutput.trim()); | ||||||
|  |         return versionOutput.toUpperCase().includes("GNU TAR"); | ||||||
|  |     }); | ||||||
|  | } | ||||||
|  | exports.isGnuTar = isGnuTar; | ||||||
|  | function getTarPath(args) { | ||||||
|     return __awaiter(this, void 0, void 0, function* () { |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|         // Explicitly use BSD Tar on Windows
 |         // Explicitly use BSD Tar on Windows
 | ||||||
|         const IS_WINDOWS = process.platform === "win32"; |         const IS_WINDOWS = process.platform === "win32"; | ||||||
| @ -4950,22 +4968,21 @@ function getTarPath() { | |||||||
|             if (fs_1.existsSync(systemTar)) { |             if (fs_1.existsSync(systemTar)) { | ||||||
|                 return systemTar; |                 return systemTar; | ||||||
|             } |             } | ||||||
|  |             else if (isGnuTar()) { | ||||||
|  |                 args.push("--force-local"); | ||||||
|  |             } | ||||||
|         } |         } | ||||||
|         return yield io.which("tar", true); |         return yield io.which("tar", true); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| function execTar(args, cwd) { | function execTar(args, cwd) { | ||||||
|     var _a, _b; |     var _a; | ||||||
|     return __awaiter(this, void 0, void 0, function* () { |     return __awaiter(this, void 0, void 0, function* () { | ||||||
|         try { |         try { | ||||||
|             yield exec_1.exec(`"${yield getTarPath()}"`, args, { cwd: cwd }); |             yield exec_1.exec(`"${yield getTarPath(args)}"`, args, { cwd: cwd }); | ||||||
|         } |         } | ||||||
|         catch (error) { |         catch (error) { | ||||||
|             const IS_WINDOWS = process.platform === "win32"; |             throw new Error(`Tar failed with error: ${(_a = error) === null || _a === void 0 ? void 0 : _a.message}`); | ||||||
|             if (IS_WINDOWS) { |  | ||||||
|                 throw new Error(`Tar failed with error: ${(_a = error) === null || _a === void 0 ? void 0 : _a.message}. Ensure BSD tar is installed and on the PATH.`); |  | ||||||
|             } |  | ||||||
|             throw new Error(`Tar failed with error: ${(_b = error) === null || _b === void 0 ? void 0 : _b.message}`); |  | ||||||
|         } |         } | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| @ -4978,7 +4995,14 @@ function extractTar(archivePath) { | |||||||
|         // Create directory to extract tar into
 |         // Create directory to extract tar into
 | ||||||
|         const workingDirectory = getWorkingDirectory(); |         const workingDirectory = getWorkingDirectory(); | ||||||
|         yield io.mkdirP(workingDirectory); |         yield io.mkdirP(workingDirectory); | ||||||
|         const args = ["-xz", "-f", archivePath, "-P", "-C", workingDirectory]; |         const args = [ | ||||||
|  |             "-xz", | ||||||
|  |             "-f", | ||||||
|  |             archivePath.replace(new RegExp("\\" + path.sep, "g"), "/"), | ||||||
|  |             "-P", | ||||||
|  |             "-C", | ||||||
|  |             workingDirectory.replace(new RegExp("\\" + path.sep, "g"), "/") | ||||||
|  |         ]; | ||||||
|         yield execTar(args); |         yield execTar(args); | ||||||
|     }); |     }); | ||||||
| } | } | ||||||
| @ -4992,10 +5016,10 @@ function createTar(archiveFolder, sourceDirectories) { | |||||||
|         const args = [ |         const args = [ | ||||||
|             "-cz", |             "-cz", | ||||||
|             "-f", |             "-f", | ||||||
|             constants_1.CacheFilename, |             constants_1.CacheFilename.replace(new RegExp("\\" + path.sep, "g"), "/"), | ||||||
|             "-P", |             "-P", | ||||||
|             "-C", |             "-C", | ||||||
|             workingDirectory, |             workingDirectory.replace(new RegExp("\\" + path.sep, "g"), "/"), | ||||||
|             "--files-from", |             "--files-from", | ||||||
|             manifestFilename |             manifestFilename | ||||||
|         ]; |         ]; | ||||||
|  | |||||||
							
								
								
									
										43
									
								
								src/tar.ts
									
									
									
									
									
								
							
							
						
						
									
										43
									
								
								src/tar.ts
									
									
									
									
									
								
							| @ -1,3 +1,4 @@ | |||||||
|  | import * as core from "@actions/core"; | ||||||
| import { exec } from "@actions/exec"; | import { exec } from "@actions/exec"; | ||||||
| import * as io from "@actions/io"; | import * as io from "@actions/io"; | ||||||
| import { existsSync, writeFileSync } from "fs"; | import { existsSync, writeFileSync } from "fs"; | ||||||
| @ -5,13 +6,32 @@ import * as path from "path"; | |||||||
| 
 | 
 | ||||||
| import { CacheFilename } from "./constants"; | import { CacheFilename } from "./constants"; | ||||||
| 
 | 
 | ||||||
| async function getTarPath(): Promise<string> { | export async function isGnuTar(): Promise<boolean> { | ||||||
|  |     core.debug("Checking tar --version"); | ||||||
|  |     let versionOutput = ""; | ||||||
|  |     await exec("tar --version", [], { | ||||||
|  |         ignoreReturnCode: true, | ||||||
|  |         silent: true, | ||||||
|  |         listeners: { | ||||||
|  |             stdout: (data: Buffer): string => | ||||||
|  |                 (versionOutput += data.toString()), | ||||||
|  |             stderr: (data: Buffer): string => (versionOutput += data.toString()) | ||||||
|  |         } | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     core.debug(versionOutput.trim()); | ||||||
|  |     return versionOutput.toUpperCase().includes("GNU TAR"); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | async function getTarPath(args: string[]): Promise<string> { | ||||||
|     // Explicitly use BSD Tar on Windows
 |     // Explicitly use BSD Tar on Windows
 | ||||||
|     const IS_WINDOWS = process.platform === "win32"; |     const IS_WINDOWS = process.platform === "win32"; | ||||||
|     if (IS_WINDOWS) { |     if (IS_WINDOWS) { | ||||||
|         const systemTar = `${process.env["windir"]}\\System32\\tar.exe`; |         const systemTar = `${process.env["windir"]}\\System32\\tar.exe`; | ||||||
|         if (existsSync(systemTar)) { |         if (existsSync(systemTar)) { | ||||||
|             return systemTar; |             return systemTar; | ||||||
|  |         } else if (isGnuTar()) { | ||||||
|  |             args.push("--force-local"); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|     return await io.which("tar", true); |     return await io.which("tar", true); | ||||||
| @ -19,14 +39,8 @@ async function getTarPath(): Promise<string> { | |||||||
| 
 | 
 | ||||||
| async function execTar(args: string[], cwd?: string): Promise<void> { | async function execTar(args: string[], cwd?: string): Promise<void> { | ||||||
|     try { |     try { | ||||||
|         await exec(`"${await getTarPath()}"`, args, { cwd: cwd }); |         await exec(`"${await getTarPath(args)}"`, args, { cwd: cwd }); | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|         const IS_WINDOWS = process.platform === "win32"; |  | ||||||
|         if (IS_WINDOWS) { |  | ||||||
|             throw new Error( |  | ||||||
|                 `Tar failed with error: ${error?.message}. Ensure BSD tar is installed and on the PATH.` |  | ||||||
|             ); |  | ||||||
|         } |  | ||||||
|         throw new Error(`Tar failed with error: ${error?.message}`); |         throw new Error(`Tar failed with error: ${error?.message}`); | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -39,7 +53,14 @@ export async function extractTar(archivePath: string): Promise<void> { | |||||||
|     // Create directory to extract tar into
 |     // Create directory to extract tar into
 | ||||||
|     const workingDirectory = getWorkingDirectory(); |     const workingDirectory = getWorkingDirectory(); | ||||||
|     await io.mkdirP(workingDirectory); |     await io.mkdirP(workingDirectory); | ||||||
|     const args = ["-xz", "-f", archivePath, "-P", "-C", workingDirectory]; |     const args = [ | ||||||
|  |         "-xz", | ||||||
|  |         "-f", | ||||||
|  |         archivePath.replace(new RegExp("\\" + path.sep, "g"), "/"), | ||||||
|  |         "-P", | ||||||
|  |         "-C", | ||||||
|  |         workingDirectory.replace(new RegExp("\\" + path.sep, "g"), "/") | ||||||
|  |     ]; | ||||||
|     await execTar(args); |     await execTar(args); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| @ -58,10 +79,10 @@ export async function createTar( | |||||||
|     const args = [ |     const args = [ | ||||||
|         "-cz", |         "-cz", | ||||||
|         "-f", |         "-f", | ||||||
|         CacheFilename, |         CacheFilename.replace(new RegExp("\\" + path.sep, "g"), "/"), | ||||||
|         "-P", |         "-P", | ||||||
|         "-C", |         "-C", | ||||||
|         workingDirectory, |         workingDirectory.replace(new RegExp("\\" + path.sep, "g"), "/"), | ||||||
|         "--files-from", |         "--files-from", | ||||||
|         manifestFilename |         manifestFilename | ||||||
|     ]; |     ]; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user
	 Aiqiao Yan
						Aiqiao Yan