mirror of
https://github.com/Cheviiot/Vintner.git
synced 2026-08-03 15:57:24 +00:00
Add DirectX SDK (D3DX9) download support
download --with-dxsdk fetches the DirectX SDK (June 2010) - the last standalone release of D3DX9/10/11, XInput and XAudio2, dropped from the Windows SDK entirely once D3DX was deprecated, but still needed by plenty of legacy code. Like the WDK, it isn't part of the VS installer manifest, so this is its own self-contained fetch+unpack path: the installer is a self-extracting CAB, unpacked directly via cabextract (already a prerequisite for the WinSDK .msi payloads) with its -F filter restricting extraction to just Include/ and Lib/ - about 21MB out of the installer's 1.2GB uncompressed payload. Verified against the real installer: real d3dx9.h and d3dx9.lib (x86 and x64) extracted correctly and linked into an actual legacy game client build.
This commit is contained in:
@@ -8,8 +8,9 @@ vintner cross-compiles with the real MSVC toolchain on Linux, using Wine.
|
|||||||
One Go binary drops in as `cl`, `link`, `lib`, `rc`, `midl`, `mc`, `mt`,
|
One Go binary drops in as `cl`, `link`, `lib`, `rc`, `midl`, `mc`, `mt`,
|
||||||
`dumpbin`, `msbuild`, `nmake`, `ml`, `ml64`, `armasm`, `armasm64`, plus
|
`dumpbin`, `msbuild`, `nmake`, `ml`, `ml64`, `armasm`, `armasm64`, plus
|
||||||
`cmd`/`findstr` shims, so once installed you invoke the real Microsoft
|
`cmd`/`findstr` shims, so once installed you invoke the real Microsoft
|
||||||
tools exactly like on Windows. It handles full MSBuild projects, and with
|
tools exactly like on Windows. It handles full MSBuild projects, with
|
||||||
`--with-wdk`, real KMDF/UMDF Windows drivers.
|
`--with-wdk` real KMDF/UMDF Windows drivers, and with `--with-dxsdk` the
|
||||||
|
real D3DX9 headers/libs.
|
||||||
|
|
||||||
Inspired by [mstorsjo/msvc-wine](https://github.com/mstorsjo/msvc-wine)'s
|
Inspired by [mstorsjo/msvc-wine](https://github.com/mstorsjo/msvc-wine)'s
|
||||||
approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
|
approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
|
||||||
@@ -21,6 +22,7 @@ approach: download the real MSVC/WinSDK, wrap the compiler under Wine.
|
|||||||
- [Quick start](#quick-start)
|
- [Quick start](#quick-start)
|
||||||
- [Commands](#commands)
|
- [Commands](#commands)
|
||||||
- [Building drivers (WDK)](#building-drivers-wdk)
|
- [Building drivers (WDK)](#building-drivers-wdk)
|
||||||
|
- [Building against D3DX9 (DirectX SDK)](#building-against-d3dx9-directx-sdk)
|
||||||
- [Language](#language)
|
- [Language](#language)
|
||||||
- [Shell completion](#shell-completion)
|
- [Shell completion](#shell-completion)
|
||||||
- [Using clang-cl/lld-link instead of Wine](#using-clang-cllld-link-instead-of-wine)
|
- [Using clang-cl/lld-link instead of Wine](#using-clang-cllld-link-instead-of-wine)
|
||||||
@@ -112,12 +114,13 @@ vintner completion bash|zsh print a shell
|
|||||||
|
|
||||||
`download`'s main options: `--msvc-version`, `--sdk-version`,
|
`download`'s main options: `--msvc-version`, `--sdk-version`,
|
||||||
`--architecture` (repeatable: `x86`/`x64`/`arm`/`arm64`/`host`),
|
`--architecture` (repeatable: `x86`/`x64`/`arm`/`arm64`/`host`),
|
||||||
`--host-arch`, `--only-host`, `--with-wdk` (see below), `--ignore`
|
`--host-arch`, `--only-host`, `--with-wdk` (see below), `--with-dxsdk`
|
||||||
(repeatable), `--only-download`, `--only-unpack`, `--keep-unpack`,
|
(see below), `--ignore` (repeatable), `--only-download`, `--only-unpack`,
|
||||||
`--skip-patch`, `--cache`, `--language`, `--include-optional`,
|
`--keep-unpack`, `--skip-patch`, `--cache`, `--language`,
|
||||||
`--skip-recommended`, `--major`, `--preview`, `--manifest`,
|
`--include-optional`, `--skip-recommended`, `--major`, `--preview`,
|
||||||
`--list-workloads`, `--list-components`, `--print-deps-tree`. Run
|
`--manifest`, `--list-workloads`, `--list-components`,
|
||||||
`vintner download -h` for the full list with descriptions.
|
`--print-deps-tree`. Run `vintner download -h` for the full list with
|
||||||
|
descriptions.
|
||||||
|
|
||||||
`--list-workloads`/`--list-components` print every workload/component id
|
`--list-workloads`/`--list-components` print every workload/component id
|
||||||
and its human-readable title from the fetched manifest, then exit
|
and its human-readable title from the fetched manifest, then exit
|
||||||
@@ -143,6 +146,22 @@ work under Wine. Tested against a real sample driver from
|
|||||||
Only x64 and arm64 targets have a WDK package upstream; there's no x86 or
|
Only x64 and arm64 targets have a WDK package upstream; there's no x86 or
|
||||||
arm one.
|
arm one.
|
||||||
|
|
||||||
|
## Building against D3DX9 (DirectX SDK)
|
||||||
|
|
||||||
|
`--with-dxsdk` fetches the DirectX SDK (June 2010) — the last standalone
|
||||||
|
release of D3DX9/10/11, XInput and XAudio2, dropped from the Windows SDK
|
||||||
|
entirely once D3DX was deprecated. It unpacks the real headers and x86/x64
|
||||||
|
import libs (`d3dx9.h`/`d3dx9.lib` included) to `<dest>/DXSDK`.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vintner download --accept-license --with-dxsdk
|
||||||
|
```
|
||||||
|
|
||||||
|
Point your project's `IncludePath`/`LibraryPath` at
|
||||||
|
`<dest>/DXSDK/Include` and `<dest>/DXSDK/Lib/x86` or `<dest>/DXSDK/Lib/x64`.
|
||||||
|
Requires `cabextract` on `PATH` (the installer is a self-extracting CAB
|
||||||
|
archive).
|
||||||
|
|
||||||
## Language
|
## Language
|
||||||
|
|
||||||
CLI text (usage, progress lines, prompts) defaults to English. Set
|
CLI text (usage, progress lines, prompts) defaults to English. Set
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ func runDownload(args []string) int {
|
|||||||
listComponents := fs.Bool("list-components", false, "list available components from the manifest and exit, without downloading anything")
|
listComponents := fs.Bool("list-components", false, "list available components from the manifest and exit, without downloading anything")
|
||||||
printDepsTree := fs.Bool("print-deps-tree", false, "print the dependency tree of the selected packages and exit, without downloading anything")
|
printDepsTree := fs.Bool("print-deps-tree", false, "print the dependency tree of the selected packages and exit, without downloading anything")
|
||||||
withWDK := fs.Bool("with-wdk", false, "also fetch and install the Windows Driver Kit (headers, libs and MSBuild driver PlatformToolsets, for building KMDF/UMDF drivers)")
|
withWDK := fs.Bool("with-wdk", false, "also fetch and install the Windows Driver Kit (headers, libs and MSBuild driver PlatformToolsets, for building KMDF/UMDF drivers)")
|
||||||
|
withDXSDK := fs.Bool("with-dxsdk", false, "also fetch and install the DirectX SDK (June 2010): real D3DX9/10/11, XInput and XAudio2 headers and import libs, dropped from the modern Windows SDK")
|
||||||
var archsFlag stringList
|
var archsFlag stringList
|
||||||
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
|
fs.Var(&archsFlag, "architecture", "target architecture to include (x86, x64, arm, arm64, host); repeatable")
|
||||||
var ignoreFlag stringList
|
var ignoreFlag stringList
|
||||||
@@ -219,6 +220,13 @@ func runDownload(args []string) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if *withDXSDK && !*onlyUnpack {
|
||||||
|
if err := downloadDXSDK(cache, destAbs); err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "vintner download:", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fmt.Println(i18n.T("download.done", destAbs))
|
fmt.Println(i18n.T("download.done", destAbs))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -255,6 +263,18 @@ func downloadWDK(opts *download.Options, selected []*download.Package, cache, de
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// downloadDXSDK fetches and unpacks the DirectX SDK (June 2010) into
|
||||||
|
// destAbs/DXSDK. See internal/download/dxsdk.go for why this is a separate
|
||||||
|
// download path from the rest of ExpandSelection/FetchPayloads/Unpack.
|
||||||
|
func downloadDXSDK(cache, destAbs string) error {
|
||||||
|
dxsdkDir, err := download.DownloadDXSDK(cache, destAbs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Print(i18n.T("download.dxsdk_installed", dxsdkDir))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func contains(list []string, v string) bool {
|
func contains(list []string, v string) bool {
|
||||||
for _, s := range list {
|
for _, s := range list {
|
||||||
if s == v {
|
if s == v {
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package download
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The DirectX SDK (June 2010) is the last standalone release of D3DX9 (and
|
||||||
|
// D3DX10/11, XInput, XAudio2, ...). Microsoft never carried D3DX forward
|
||||||
|
// into the Windows 10/11 SDK - it's deprecated in favor of D3DCompiler/
|
||||||
|
// WICTextureLoader/DirectXTex, but plenty of legacy code (this included)
|
||||||
|
// still links against the real d3dx9.h/d3dx9.lib. Like the WDK (see
|
||||||
|
// wdk.go), it isn't part of the VS installer manifest or any package feed
|
||||||
|
// vsman knows about, so this is a separate, self-contained download path
|
||||||
|
// outside ExpandSelection/FetchPayloads/UnpackSelectedPackages.
|
||||||
|
//
|
||||||
|
// The installer is a self-extracting PE with an appended CAB archive.
|
||||||
|
// cabextract (already a vintner prerequisite - see runMsiExtract's sibling
|
||||||
|
// extractWindowsSDKPackage) unpacks it directly, without needing Wine or a
|
||||||
|
// separate archive tool. Its -F/--filter flag (repeatable) restricts
|
||||||
|
// extraction to the Include and Lib subtrees actually needed for building -
|
||||||
|
// about 21MB out of the installer's 1.2GB uncompressed payload.
|
||||||
|
|
||||||
|
const dxsdkURL = "https://download.microsoft.com/download/A/E/7/AE743F1F-632B-4809-87A9-AA1BB3458E31/DXSDK_Jun10.exe"
|
||||||
|
|
||||||
|
// dxsdkSHA256 pins the exact installer build this code was written against
|
||||||
|
// (verified by fully extracting it with both cabextract and 7z and cross
|
||||||
|
// checking the file lists) - the June 2010 DirectX SDK is a frozen legacy
|
||||||
|
// artifact Microsoft is not going to rebuild. A var, not a const, so tests
|
||||||
|
// can point it at a small fake payload instead of the real 600MB installer.
|
||||||
|
var dxsdkSHA256 = "705271dc83bfee54d9b94e028426e288d5f070784b7446d164f48ecfbb2a02cb"
|
||||||
|
|
||||||
|
// DownloadDXSDK fetches (or reuses a cached copy of) the DirectX SDK (June
|
||||||
|
// 2010) installer into cacheDir, then unpacks its Include and Lib trees -
|
||||||
|
// headers and x86/x64 import libs for D3DX9/10/11, XInput, XAudio2, and the
|
||||||
|
// rest - into destDir/DXSDK. Returns that directory.
|
||||||
|
func DownloadDXSDK(cacheDir, destDir string) (string, error) {
|
||||||
|
if _, err := exec.LookPath("cabextract"); err != nil {
|
||||||
|
return "", fmt.Errorf("cabextract not found in PATH (install the cabextract package): %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheFile := filepath.Join(cacheDir, "DXSDK_Jun10.exe")
|
||||||
|
if !isFile(cacheFile) {
|
||||||
|
fmt.Println("Downloading DirectX SDK (June 2010)")
|
||||||
|
if err := httpDownloadFile(dxsdkURL, cacheFile); err != nil {
|
||||||
|
return "", fmt.Errorf("downloading DirectX SDK: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("Using existing file", filepath.Base(cacheFile))
|
||||||
|
}
|
||||||
|
sum, err := sha256File(cacheFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !equalFoldHex(sum, dxsdkSHA256) {
|
||||||
|
return "", fmt.Errorf("incorrect hash for downloaded file %s, aborting", filepath.Base(cacheFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
scratch, err := os.MkdirTemp(destDir, "dxsdk-unpack-")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(scratch)
|
||||||
|
|
||||||
|
if err := runCabextract(cacheFile, scratch, "DXSDK/Include/*", "DXSDK/Lib/*"); err != nil {
|
||||||
|
return "", fmt.Errorf("extracting DirectX SDK: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dxsdkDir := filepath.Join(destDir, "DXSDK")
|
||||||
|
if err := combineDirTrees(filepath.Join(scratch, "DXSDK"), dxsdkDir); err != nil {
|
||||||
|
return "", fmt.Errorf("moving DirectX SDK content into place: %w", err)
|
||||||
|
}
|
||||||
|
return dxsdkDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// runCabextract extracts srcFile into destDir, restricted to entries
|
||||||
|
// matching any of patterns (cabextract's -F, repeatable, glob-matched
|
||||||
|
// against the full in-archive path).
|
||||||
|
func runCabextract(srcFile, destDir string, patterns ...string) error {
|
||||||
|
if err := os.MkdirAll(destDir, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
args := []string{"-q", "-d", destDir}
|
||||||
|
for _, p := range patterns {
|
||||||
|
args = append(args, "-F", p)
|
||||||
|
}
|
||||||
|
args = append(args, srcFile)
|
||||||
|
cmd := exec.Command("cabextract", args...)
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
return cmd.Run()
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
package download
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// withFakeCabextract prepends a directory containing a fake "cabextract"
|
||||||
|
// script to PATH, so tests can exercise runCabextract/DownloadDXSDK without
|
||||||
|
// the real tool (or a real DXSDK installer) present. The fake script
|
||||||
|
// records the arguments it was invoked with to argsFile and creates an
|
||||||
|
// empty DXSDK/Include and DXSDK/Lib under whatever -d directory it was
|
||||||
|
// given, mimicking a successful (if empty) extraction.
|
||||||
|
func withFakeCabextract(t *testing.T, argsFile string) {
|
||||||
|
t.Helper()
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skip("fake cabextract script requires a POSIX shell")
|
||||||
|
}
|
||||||
|
bin := t.TempDir()
|
||||||
|
script := `#!/bin/sh
|
||||||
|
echo "$@" > "` + argsFile + `"
|
||||||
|
dest=""
|
||||||
|
prev=""
|
||||||
|
for a in "$@"; do
|
||||||
|
if [ "$prev" = "-d" ]; then
|
||||||
|
dest="$a"
|
||||||
|
fi
|
||||||
|
prev="$a"
|
||||||
|
done
|
||||||
|
mkdir -p "$dest/DXSDK/Include" "$dest/DXSDK/Lib"
|
||||||
|
`
|
||||||
|
path := filepath.Join(bin, "cabextract")
|
||||||
|
if err := os.WriteFile(path, []byte(script), 0o755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("PATH", bin+":"+os.Getenv("PATH"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunCabextractPassesFilterArgs(t *testing.T) {
|
||||||
|
dest := t.TempDir()
|
||||||
|
argsFile := filepath.Join(t.TempDir(), "args")
|
||||||
|
withFakeCabextract(t, argsFile)
|
||||||
|
|
||||||
|
src := filepath.Join(t.TempDir(), "installer.exe")
|
||||||
|
writeFile(t, src, "fake-installer-bytes")
|
||||||
|
|
||||||
|
if err := runCabextract(src, dest, "DXSDK/Include/*", "DXSDK/Lib/*"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := os.ReadFile(argsFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected the fake cabextract to have run: %v", err)
|
||||||
|
}
|
||||||
|
want := "-q -d " + dest + " -F DXSDK/Include/* -F DXSDK/Lib/* " + src + "\n"
|
||||||
|
if string(got) != want {
|
||||||
|
t.Errorf("cabextract args = %q, want %q", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadDXSDKReusesCachedFileAndRejectsHashMismatch(t *testing.T) {
|
||||||
|
argsFile := filepath.Join(t.TempDir(), "args")
|
||||||
|
withFakeCabextract(t, argsFile)
|
||||||
|
|
||||||
|
cacheDir := t.TempDir()
|
||||||
|
destDir := t.TempDir()
|
||||||
|
writeFile(t, filepath.Join(cacheDir, "DXSDK_Jun10.exe"), "not the real installer")
|
||||||
|
|
||||||
|
_, err := DownloadDXSDK(cacheDir, destDir)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected an error for a cached file that doesn't match dxsdkSHA256")
|
||||||
|
}
|
||||||
|
if _, statErr := os.Stat(argsFile); statErr == nil {
|
||||||
|
t.Error("cabextract should not have run before the hash was verified")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDownloadDXSDKMovesExtractedContentIntoPlace(t *testing.T) {
|
||||||
|
argsFile := filepath.Join(t.TempDir(), "args")
|
||||||
|
withFakeCabextract(t, argsFile)
|
||||||
|
|
||||||
|
cacheDir := t.TempDir()
|
||||||
|
destDir := t.TempDir()
|
||||||
|
cacheFile := filepath.Join(cacheDir, "DXSDK_Jun10.exe")
|
||||||
|
writeFile(t, cacheFile, "fake-installer-bytes-for-hash-test")
|
||||||
|
|
||||||
|
// Patch the expected hash to match our fake cached file, since we can't
|
||||||
|
// (and shouldn't) fetch or embed the real 600MB installer in a test.
|
||||||
|
sum, err := sha256File(cacheFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
restore := dxsdkSHA256
|
||||||
|
dxsdkSHA256 = sum
|
||||||
|
defer func() { dxsdkSHA256 = restore }()
|
||||||
|
|
||||||
|
dxsdkDir, err := DownloadDXSDK(cacheDir, destDir)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if dxsdkDir != filepath.Join(destDir, "DXSDK") {
|
||||||
|
t.Errorf("DownloadDXSDK returned %q, want %q", dxsdkDir, filepath.Join(destDir, "DXSDK"))
|
||||||
|
}
|
||||||
|
for _, sub := range []string{"Include", "Lib"} {
|
||||||
|
if !isDir(filepath.Join(dxsdkDir, sub)) {
|
||||||
|
t.Errorf("expected %s/%s to exist after extraction", dxsdkDir, sub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,8 +77,8 @@ Usage:
|
|||||||
vintner completion bash|zsh print a shell completion script
|
vintner completion bash|zsh print a shell completion script
|
||||||
|
|
||||||
Run "vintner <command> --help" for that command's own options - download
|
Run "vintner <command> --help" for that command's own options - download
|
||||||
has many, including --with-wdk, --list-workloads, --list-components and
|
has many, including --with-wdk, --with-dxsdk, --list-workloads,
|
||||||
--print-deps-tree.
|
--list-components and --print-deps-tree.
|
||||||
|
|
||||||
--dest/[dir] default to ~/.vintner if omitted.
|
--dest/[dir] default to ~/.vintner if omitted.
|
||||||
Language: set VINTNER_LANG=ru (or LANG=ru_RU...) for Russian output.
|
Language: set VINTNER_LANG=ru (or LANG=ru_RU...) for Russian output.
|
||||||
@@ -99,8 +99,8 @@ Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
|
|||||||
vintner completion bash|zsh вывести скрипт автодополнения для оболочки
|
vintner completion bash|zsh вывести скрипт автодополнения для оболочки
|
||||||
|
|
||||||
Запустите «vintner <команда> --help» для параметров конкретной команды —
|
Запустите «vintner <команда> --help» для параметров конкретной команды —
|
||||||
у download их много, включая --with-wdk, --list-workloads, --list-components
|
у download их много, включая --with-wdk, --with-dxsdk, --list-workloads,
|
||||||
и --print-deps-tree.
|
--list-components и --print-deps-tree.
|
||||||
|
|
||||||
--dest/[каталог] по умолчанию — ~/.vintner.
|
--dest/[каталог] по умолчанию — ~/.vintner.
|
||||||
Язык: установите VINTNER_LANG=en (или LANG=en_US...) для вывода на английском.
|
Язык: установите VINTNER_LANG=en (или LANG=en_US...) для вывода на английском.
|
||||||
@@ -161,6 +161,10 @@ Once installed, add <dir>/bin/<arch> to PATH and invoke the tools directly:
|
|||||||
EN: "Installed WDK (%s) %s at %s\n",
|
EN: "Installed WDK (%s) %s at %s\n",
|
||||||
RU: "WDK (%s) %s установлен в %s\n",
|
RU: "WDK (%s) %s установлен в %s\n",
|
||||||
},
|
},
|
||||||
|
"download.dxsdk_installed": {
|
||||||
|
EN: "Installed DirectX SDK (June 2010) at %s\n",
|
||||||
|
RU: "DirectX SDK (июнь 2010) установлен в %s\n",
|
||||||
|
},
|
||||||
"download.workloads_header": {
|
"download.workloads_header": {
|
||||||
EN: "Available Workloads (%d):\n",
|
EN: "Available Workloads (%d):\n",
|
||||||
RU: "Доступные рабочие нагрузки (Workload) (%d):\n",
|
RU: "Доступные рабочие нагрузки (Workload) (%d):\n",
|
||||||
|
|||||||
Reference in New Issue
Block a user