// Package dll contains dll-handling code, specifically for "enabling" and "disabling" them in a wine env.
package dll
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"strings"
"git.sr.ht/~hristoast/wem/opt"
)
const DxvkKind = "dxvk"
const DxvkReleaseURL = "https://api.github.com/repos/doitsujin/dxvk/releases"
const Vkd3dKind = "vkd3d-proton"
const Vkd3dReleaseURL = "https://api.github.com/repos/HansKristian-Work/vkd3d-proton/releases"
const all32dir = "x64"
const backupExt = ".orig-wem"
const gzipKind = "gz"
const xzKind = "xz"
const zstKind = "zst"
const dxvk64dir = "x32"
const dxvkTarballURL = "https://github.com/doitsujin/dxvk/releases/download/v%s/dxvk-%s.tar.gz"
const vkd3d64dir = "x86"
const vkd3dTarballURL = "https://github.com/HansKristian-Work/vkd3d-proton/releases/download/v%s/vkd3d-proton-%s.tar.zst"
const testKind = "test"
const testTarballURL = "https://wem.hristos.co/files/test-1.2.3.tar.zst"
var dxvkDLLs = []string{
"dxgi.dll",
"d3d11.dll",
"d3d10core.dll",
"d3d9.dll",
}
var vkd3dDLLs = []string{"d3d12.dll"}
var testDLLs = []string{"wemTest.dll"}
type dLL struct {
cacheDir string
file string
kind string
wineArch string
winePrefix string
dxvkVersion string
vkd3dVersion string
}
// DisableAll disables all DLLs of a given kind. This includes restoring any original DLLs that were backed up.
func DisableAll(has64 bool, cacheDir, kind, wineArch, winePrefix, dllVersion string) error {
var dlls []string
switch kind {
case DxvkKind:
dlls = dxvkDLLs
case Vkd3dKind:
dlls = vkd3dDLLs
case testKind:
dlls = testDLLs
}
disabled := false
var err error
for _, dll := range dlls {
d := &dLL{
cacheDir: cacheDir,
file: dll,
kind: kind,
wineArch: wineArch,
winePrefix: winePrefix,
}
switch kind {
case DxvkKind:
d.dxvkVersion = dllVersion
case Vkd3dKind:
d.vkd3dVersion = dllVersion
}
dDir32, dDir64 := d.destDirs(has64)
dst32, dst64 := d.destPaths(has64, dDir32, dDir64)
bfp32, bfp64 := d.backupFilePaths(has64, dst32, dst64)
fil32, fil64 := d.areFiles(has64, dst32, dst64)
sym32, sym64 := d.areSymlinks(has64, dst32, dst64)
src32, src64 := d.srcPaths(has64, d.version())
vln32, vln64 := d.linksAreValid(has64, dst32, dst64, src32, src64)
disabled, err = d.disable(bfp32, bfp64, dst32, dst64, has64, fil32, fil64, sym32, sym64, vln32, vln64)
if err != nil {
return err
}
}
if disabled {
log.Printf("Disable %s", strings.Split(strings.ToUpper(kind), "-")[0])
}
return nil
}
// EnableAll enables all DLLs of a given kind. This includes backing up any original DLLs that may exist.
func EnableAll(has64 bool, cacheDir, kind, wineArch, winePrefix, dllVersion string) error {
var dlls []string
switch kind {
case DxvkKind:
dlls = dxvkDLLs
case Vkd3dKind:
dlls = vkd3dDLLs
case testKind:
dlls = testDLLs
}
for _, dll := range dlls {
d := &dLL{
cacheDir: cacheDir,
file: dll,
kind: kind,
wineArch: wineArch,
winePrefix: winePrefix,
}
switch kind {
case DxvkKind:
d.dxvkVersion = dllVersion
case Vkd3dKind:
d.vkd3dVersion = dllVersion
}
ver := d.version()
dDir32, dDir64 := d.destDirs(has64)
dst32, dst64 := d.destPaths(has64, dDir32, dDir64)
bfp32, bfp64 := d.backupFilePaths(has64, dst32, dst64)
bak32, bak64 := d.areBackedup(has64, bfp32, bfp64)
isf32, isf64 := d.areFiles(has64, dst32, dst64)
src32, src64 := d.srcPaths(has64, ver)
sym32, sym64 := d.areSymlinks(has64, dst32, dst64)
vln32, vln64 := d.linksAreValid(has64, dst32, dst64, src32, src64)
err := d.enable(bfp32, bfp64, dst32, dst64, src32, src64, ver,
has64, bak32, bak64, isf32, isf64, sym32, sym64, vln32, vln64)
if err != nil {
return err
}
}
return nil
}
// IsFile returns true after a given path has been reasobaly
// checked to be an actual file and not a directory or symlink.
func IsFile(path string) bool {
// Does it exist?
stat, errStat := os.Stat(path)
// Is it a symlink?
src, errReadLink := os.Readlink(path)
// Check that the given path actually exists first...
if stat != nil {
if (!stat.IsDir() && errStat == nil) && (src == "" && errReadLink != nil) {
// It's not a dir, exists, and isn't a symlink
return true
}
}
// Probably not a file
return false
}
// IsSymlink returns true after a given path has been reasonably checked
// to be an actual symlink that resolves and not a directory or file.
func IsSymlink(path string) bool {
src, err := os.Readlink(path)
if src != "" && err == nil {
return true
}
return false
}
// ValidLink tells if the given linkPath actually resolves to the given srcPath
func ValidLink(linkPath, srcPath string) bool {
src, err := os.Readlink(linkPath)
if err != nil {
return false
}
_, err = os.Stat(src)
if err != nil {
return false
}
return src == srcPath
}
// Call this with defer to get notified about errors when closing files.
func closerNotifier(file io.Closer) {
err := file.Close()
if err != nil {
log.Println(err)
}
}
// Return true if the DLLs are backed up
func (d *dLL) areBackedup(has64 bool, bakFilePth32, bakFilePth64 string) (bool, bool) {
return IsFile(bakFilePth32), IsFile(bakFilePth64)
}
// Return true if the DLLs are files at their destination paths
func (d *dLL) areFiles(has64 bool, dstPth32, dstPth64 string) (bool, bool) {
f32, f64 := false, false
f32 = IsFile(dstPth32)
if has64 {
f64 = IsFile(dstPth64)
}
return f32, f64
}
// Return true if the DLL is a symlink at the destination path
func (d *dLL) areSymlinks(has64 bool, dstPth32, dstPth64 string) (bool, bool) {
s32, s64 := false, false
s32 = IsSymlink(dstPth32)
if has64 {
s64 = IsSymlink(dstPth64)
}
return s32, s64
}
// Copy the given dst file to the bfp path.
func (d *dLL) backup(bfp, dst string) error {
_, err := os.Stat(bfp)
if err != nil {
orig, err := os.Open(dst)
if err != nil {
return err
}
defer closerNotifier(orig)
bkup, err := os.Create(bfp)
if err != nil {
return err
}
defer closerNotifier(bkup)
_, err = io.Copy(bkup, orig)
if err != nil {
return err
}
}
return nil
}
// Returns the full paths to the backup files
func (d *dLL) backupFilePaths(has64 bool, dstPth32, dstPth64 string) (string, string) {
return dstPth32 + backupExt, dstPth64 + backupExt
}
// Return the full paths to the DLL's destination directories.
// The win64 value is nil for 32-bit prefixes.
func (d *dLL) destDirs(has64 bool) (string, string) {
win32 := filepath.Join(d.winePrefix, "drive_c", "windows", "system32")
var win64 string
if has64 {
win64 = filepath.Join(d.winePrefix, "drive_c", "windows", "syswow64")
}
return win32, win64
}
// Return the full path to the DLL's destination path
func (d *dLL) destPaths(has64 bool, dstDir32, dstDir64 string) (string, string) {
win32 := filepath.Join(dstDir32, d.file)
var win64 string
if dstDir64 != "" {
win64 = filepath.Join(dstDir64, d.file)
}
return win32, win64
}
// Disable the DLL. This will restore any backed up original file and delete symlinks as needed.
func (d *dLL) disable(bakPth32, bakPth64, dstPth32, dstPth64 string, has64, isFile32, isFile64, isSymlink32, isSymlink64, isValidLink32, isValidLink64 bool) (bool, error) {
var err error
// If the dest file is a file and there's no backup file ("looks disabled"), do nothing
// OR: if neither the dest nor backup file exist, it's a fresh prefix and we should do nothing.
bpif32 := IsFile(bakPth32)
bpif64 := IsFile(bakPth64)
doNothing32, doNothing64 := false, false
// Fresh envs look like this
if !isSymlink32 && !bpif32 && !isFile32 && !isValidLink32 {
doNothing32 = true
if has64 {
doNothing64 = true
} else {
doNothing64 = true
}
}
if isFile32 && !isSymlink32 && !bpif32 && !isValidLink32 {
// The 32-bit files look disabled
doNothing32 = true
if has64 {
if isFile64 && !isSymlink64 && !bpif64 && !isValidLink64 {
// Both 32-bit and 64-bit files look disabled
doNothing64 = true
}
} else {
doNothing64 = true
}
}
if doNothing32 && doNothing64 {
return false, nil
}
// Delete destination if its a valid symlink
if isSymlink32 {
err = os.Remove(dstPth32)
if err != nil {
return false, err
}
}
// Restore backup file if needed
if bpif32 {
err = d.restore(dstPth32, bakPth32)
if err != nil {
return false, err
}
}
if has64 {
if isSymlink64 {
err = os.Remove(dstPth64)
if err != nil {
return false, err
}
if bpif64 {
err = d.restore(dstPth64, bakPth64)
if err != nil {
return false, err
}
}
}
}
return true, nil
}
// Enable the DLL. This will do nothing if things are as expected, or it will:
// 1) Download the DLL's archive as needed
// 2) Extract the DLL's archive as needed
// 3) Back up the DLL's original as needed
// 4) Erase pre-existing, invalid symlinks
// 5) Create a symlink to the DLL at the desired destination
// 6) Validate the symlink
func (d *dLL) enable(bakFilePth32, bakFilePth64, dstPth32, dstPth64, srcPth32, srcPth64, ver string, has64, isBackedUp32, isBackedUp64, isFile32, isFile64, isSymlink32, isSymlink64, isValidLink32, isValidLink64 bool) error {
var err error
// If it's already enabled, do nothing
if isSymlink32 && isValidLink32 {
if has64 {
if isSymlink64 && isValidLink64 {
return nil
}
} else {
return nil
}
}
tarKind := d.tarballKind()
tarPath := d.tarballPath(tarKind, d.srcDir(ver), ver)
// Download if needed
if !d.isDownloaded(tarKind, tarPath, ver) {
log.Printf("Downloading %s %s ...\n", strings.ToUpper(d.kind), ver)
err = opt.DownloadFile(d.tarballURL(ver), tarPath)
if err != nil {
return err
}
}
// Extract if needed
if !d.isExtracted(d.srcDir(ver)) {
log.Printf("Extracting %s ...\n", tarPath)
err = opt.ExtractTarball(tarPath, d.cacheDir, tarKind)
if err != nil {
return err
}
}
// Check the dest dll, backup if needed
if isFile32 && !isBackedUp32 {
err = d.backup(bakFilePth32, dstPth32)
if err != nil {
return err
}
// Now that the file has been backed up, remove the original
err = os.Remove(dstPth32)
if err != nil {
return err
}
}
if has64 {
if isFile64 && !isBackedUp64 {
err = d.backup(bakFilePth64, dstPth64)
if err != nil {
return err
}
err = os.Remove(dstPth64)
if err != nil {
return err
}
}
}
// If dest exists and is a symlink but is not valid, erase it
if isSymlink32 && !isValidLink32 {
err = os.Remove(dstPth32)
if err != nil {
return err
}
}
if has64 {
if isSymlink64 && !isValidLink64 {
err = os.Remove(dstPth64)
if err != nil {
return err
}
}
}
// Make symlink to source file as needed
if !isValidLink32 {
err = os.Symlink(srcPth32, dstPth32)
if err != nil {
return err
}
}
if has64 {
if !isValidLink64 {
err = os.Symlink(srcPth64, dstPth64)
if err != nil {
return err
}
}
}
// All done!
return nil
}
// Return true if the DLLs tarball is downloaded
func (d *dLL) isDownloaded(tarKind, tarPath, ver string) bool {
_, err := os.Stat(tarPath)
return err == nil
}
// Return true if the DLLs tarball is extracted
func (d *dLL) isExtracted(srcDir string) bool {
_, err := os.Stat(srcDir)
return err == nil
}
// Returns true if the destination DLL:
// 1) is a non-broken symlink, and
// 2) points to the expected source file.
func (d *dLL) linksAreValid(has64 bool, dstPth32, dstPth64, srcPth32, srcPth64 string) (bool, bool) {
var v32, v64 bool
v32 = ValidLink(dstPth32, srcPth32)
if has64 {
v64 = ValidLink(dstPth64, srcPth64)
}
return v32, v64
}
func (d *dLL) restore(dstPath, bakPath string) error {
// Try to erase the symlink, don't fail if it doesn't exist
err := os.Remove(dstPath)
if err != nil {
if !os.IsNotExist(err) {
return err
}
}
// Create a new destPath file
newDest, err := os.Create(dstPath)
if err != nil {
return err
}
defer closerNotifier(newDest)
// Open the backup file
openedBackup, err := os.Open(bakPath)
if err != nil {
return err
}
defer closerNotifier(openedBackup)
// Copy the backup path to the opened dest file path
_, err = io.Copy(newDest, openedBackup)
if err != nil {
return err
}
// Erase the backup file
err = os.Remove(bakPath)
if err != nil {
return err
}
return nil
}
// Return the full path to the DLL's source directory
func (d *dLL) srcDir(ver string) string {
return filepath.Join(d.cacheDir, fmt.Sprintf("%s-%s", d.kind, ver))
}
// Return the full paths to the DLL's source paths
func (d *dLL) srcPaths(has64 bool, ver string) (string, string) {
srcDir := d.srcDir(ver)
win32 := filepath.Join(srcDir, all32dir, d.file)
var win64 string
if has64 {
switch d.kind {
case DxvkKind:
win64 = filepath.Join(srcDir, dxvk64dir, d.file)
case Vkd3dKind, testKind:
win64 = filepath.Join(srcDir, vkd3d64dir, d.file)
}
}
return win32, win64
}
// Return a string that represents the kind of tarball the DLL comes in
func (d *dLL) tarballKind() string {
switch d.kind {
case DxvkKind:
return gzipKind
case Vkd3dKind, testKind:
return zstKind
}
return "none"
}
// Returns the full path to the DLL's downloaded tarball
func (d *dLL) tarballPath(kind, srcDir, ver string) string {
return srcDir + fmt.Sprintf(".tar.%s", kind)
}
// Returns the URL at which the DLL's tarball can be downloaded
func (d *dLL) tarballURL(ver string) string {
switch d.kind {
case DxvkKind:
return fmt.Sprintf(dxvkTarballURL, ver, ver)
case Vkd3dKind:
return fmt.Sprintf(vkd3dTarballURL, ver, ver)
case testKind:
return testTarballURL
}
return "none"
}
// Returns the version of the DLL's containing package
func (d *dLL) version() string {
switch d.kind {
case DxvkKind:
return strings.TrimPrefix(d.dxvkVersion, "v")
case Vkd3dKind:
return strings.TrimPrefix(d.vkd3dVersion, "v")
case testKind:
return "1.2.3"
}
return "none"
}