// Package env provides the wine env object.
package env
import (
"bufio"
"bytes"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"text/template"
"time"
"git.sr.ht/~hristoast/wem/cfg"
"git.sr.ht/~hristoast/wem/dll"
"git.sr.ht/~hristoast/wem/exe"
"git.sr.ht/~hristoast/wem/opt"
"github.com/pelletier/go-toml"
)
type Info struct {
Date string
Name string
}
type InstallOpts struct {
GogSilentInstall bool
InstallArgs []string
InstallExe string
InstallExpected string
InstallWorkDir string
}
type RunOpts struct {
QuietRun bool
RunArgs []string
RunExe string
RunPrefix string
RunSuffix string
RunPre string
RunPost string
RunWorkDir string
Sandbox bool
SandboxBlacklist []string
SandboxWhitelist []string
SandboxCpu int
SandboxDns string
SandboxIpcNamespace bool
SandboxMachineId bool
SandboxNetNone bool
SandboxNoDbus bool
SandboxNoPrinters bool
SandboxNoU2f bool
SandboxPrivateCache bool
SandboxPrivateCwd bool
SandboxPrivateTmp bool
}
type SysOpts struct {
ReducePulseLatency bool
RestartPulse bool
RestoreResolution bool
SingleCore bool
SysEnvVars []string
VulkanIcdLoader string
}
type WineOpts struct {
DxvkVersion string
Esync bool
Fsync bool
VirtualDesktop string
Vkd3dVersion string
WineArch string
WineArgs []string
WineDllOverrides string
WineExe string
WinePrefix string
WinetricksPath string
}
type WineEnv struct {
*Info
*InstallOpts
*RunOpts
*WineOpts
*SysOpts
}
// AsToml gives a toml representation of the env.
// This represents the configuration file contents.
func (e *WineEnv) AsToml(noUnchanged bool) (string, error) {
b, err := toml.Marshal(e)
if err != nil {
return "", err
}
var out string
tml := string(b)
if noUnchanged {
for _, line := range strings.Split(tml, "\n") {
if !strings.HasSuffix(line, " = \"\"") &&
!strings.HasSuffix(line, " = false") &&
!strings.HasSuffix(line, " = 0") &&
!strings.HasSuffix(line, " = []") {
out += line + "\n"
}
}
out = strings.TrimSuffix(out, "\n")
} else {
out = tml
}
return out, nil
}
// CfgExists indicates if the env's configuration file exists.
func (e *WineEnv) CfgExists(name string) bool {
return dll.IsFile(e.CfgPath(name)) || dll.IsSymlink(e.CfgPath(name))
}
func (e *WineEnv) CfgPath(name string) string {
return filepath.Join(cfg.CfgDir(), fmt.Sprintf("%s.cfg", name))
}
func (e *WineEnv) InstallString() string {
// RunPre InstallWorkDir SysEnvVars WineArch WinePrefix WineExe WineArgs InstallExe InstallArgs
s := &strings.Builder{}
if e.RunPre != "" {
fmt.Fprintf(s, "%s; ", e.RunPre)
}
if e.RunWorkDir != "" {
fmt.Fprintf(s, "cd %s; ", e.InstallWorkDir)
}
if e.SysEnvVars != nil {
for _, envVar := range e.SysEnvVars {
fmt.Fprintf(s, "%s ", envVar)
}
}
fmt.Fprintf(s, "WINEARCH=%s ", e.WineArch)
fmt.Fprintf(s, "WINEPREFIX=%s ", e.WinePrefix)
if e.useSandbox() {
e.writeFirejailString(s)
}
fmt.Fprintf(s, "%s ", e.WineExe)
if e.WineArgs != nil {
for _, wineArg := range e.WineArgs {
fmt.Fprintf(s, "%s ", wineArg)
}
}
fmt.Fprintf(s, "%s ", e.InstallExe)
if e.GogSilentInstall {
s.WriteString("/dir=c:\\game /verysilent /sp- /suppressmsgboxes ")
}
if e.InstallArgs != nil {
for _, installArg := range e.InstallArgs {
fmt.Fprintf(s, "%s ", installArg)
}
}
if e.useSandbox() {
s.WriteString("--")
}
return strings.TrimSuffix(s.String(), " ")
}
func (e *WineEnv) ExeString(exec string, execArgs []string) (string, error) {
// RunPre RunWorkDir SysEnvVars WineArch WinePrefix Esync Fsync ReducePulseLatency WineDllOverrides/DXVK/VKD3D RunPrefix SingleCore WineExe WineArgs VirtualDesktop RunExe/exec RunArgs RunSuffix RunPost
s := &strings.Builder{}
if exec == "" {
if e.RunPre != "" {
fmt.Fprintf(s, "%s; ", e.RunPre)
}
}
if e.RunWorkDir != "" {
fmt.Fprintf(s, "cd %s; ", e.RunWorkDir)
}
if e.SysEnvVars != nil {
for _, envVar := range e.SysEnvVars {
fmt.Fprintf(s, "%s ", envVar)
}
}
if e.DxvkVersion != "" {
s.WriteString("WINE_LARGE_ADDRESS_AWARE=1 ")
}
fmt.Fprintf(s, "WINEARCH=%s ", e.WineArch)
fmt.Fprintf(s, "WINEPREFIX=%s ", e.WinePrefix)
if e.VulkanIcdLoader != "" {
fmt.Fprintf(s, "VK_ICD_FILENAMES=%s ", e.VulkanIcdLoader)
}
if e.Esync && !e.Fsync {
s.WriteString("WINEESYNC=1 ")
}
if e.Fsync {
s.WriteString("WINEFSYNC=1 ")
}
if e.ReducePulseLatency {
s.WriteString("PULSE_LATENCY_MSEC=60 ")
}
if e.WineDllOverrides != "" || e.DxvkVersion != "" || e.Vkd3dVersion != "" {
o := &strings.Builder{}
if e.DxvkVersion != "" {
o.WriteString("d3d9,d3d10,d3d10core,d3d11=native")
}
if e.Vkd3dVersion != "" {
if o.String() != "" {
o.WriteString(";")
}
o.WriteString("d3d12=native")
}
if e.WineDllOverrides != "" {
if o.String() != "" {
o.WriteString(";")
}
o.WriteString(e.WineDllOverrides)
}
fmt.Fprintf(s, "WINEDLLOVERRIDES=%s ", o.String())
}
if e.useSandbox() {
e.writeFirejailString(s)
}
if e.RunPrefix != "" {
fmt.Fprintf(s, "%s ", e.RunPrefix)
}
if e.SingleCore {
s.WriteString("taskset -c 0 ")
}
fmt.Fprintf(s, "%s ", e.WineExe)
if e.WineArgs != nil {
for _, wineArg := range e.WineArgs {
fmt.Fprintf(s, "%s ", wineArg)
}
}
if e.VirtualDesktop != "" {
fmt.Fprintf(s, "explorer /desktop=%s ", e.VirtualDesktop)
}
if exec == "" {
s.WriteString(strings.ReplaceAll(e.RunExe, "\\", "\\\\"))
if e.RunArgs != nil {
for _, runArg := range e.RunArgs {
fmt.Fprintf(s, " %s", runArg)
}
}
if e.RunSuffix != "" {
fmt.Fprintf(s, " %s", e.RunSuffix)
}
if e.RunPost != "" {
fmt.Fprintf(s, "; %s", e.RunPost)
}
} else {
execRendered, err := e.RenderString(exec)
if err != nil {
return "", err
}
s.WriteString(strings.ReplaceAll(execRendered, " ", "\\ "))
for _, arg := range execArgs {
s.WriteString(" ")
s.WriteString(arg)
}
}
if e.useSandbox() {
s.WriteString(" --")
}
return s.String(), nil
}
func (e *WineEnv) PrefixExists() bool {
stat, err := os.Stat(e.WinePrefix)
if err != nil {
return false
}
return stat.IsDir() || dll.IsSymlink(e.WinePrefix)
}
func (e *WineEnv) ctx() (*exe.ExeCtx, error) {
ctx := &exe.ExeCtx{
Args: []string{},
Command: "",
EnvName: e.Name,
EnvVars: e.SysEnvVars,
Prefix: e.RunPrefix,
Stderr: os.Stderr,
Stdout: os.Stdout,
Suffix: e.RunSuffix,
Title: e.runTitle(),
WorkDir: e.RunWorkDir,
}
wineVars := []string{
fmt.Sprintf("WINEARCH=%s", e.WineArch),
fmt.Sprintf("WINEPREFIX=%s", e.WinePrefix),
}
ctx.EnvVars = append(ctx.EnvVars, wineVars...)
if e.QuietRun {
devnull, err := os.Open(os.DevNull)
if err != nil {
return nil, err
}
ctx.Stderr = devnull
ctx.Stdout = devnull
}
return ctx, nil
}
func (e *WineEnv) RunCtx(cacheDir string) (*exe.ExeCtx, error) {
var command string
var arguments []string
ctx, err := e.ctx()
if err != nil {
return nil, err
}
// VulkanIcdLoader
if e.VulkanIcdLoader != "" {
log.Println("Set VulkanIcdLoader")
ctx.EnvVars = append(ctx.EnvVars, fmt.Sprintf("VK_ICD_FILENAMES=%s", e.VulkanIcdLoader))
}
// ESYNC
if e.Esync && !e.Fsync {
can, err := e.wineCan("esync")
if err != nil {
return nil, err
}
log.Println("Enable ESYNC")
if !can {
log.Println("WARNING: your wine build may not support esync!")
}
ctx.EnvVars = append(ctx.EnvVars, "WINEESYNC=1")
}
if e.Esync && e.Fsync {
log.Println("WARNING: Esync has been disabled since fsync is also enabled.")
}
// FSYNC
if e.Fsync {
can, err := e.wineCan("fsync")
if err != nil {
return nil, err
}
log.Println("Enable FSYNC")
if !can {
log.Println("WARNING: Your wine build may not support fsync!")
}
ctx.EnvVars = append(ctx.EnvVars, "WINEFSYNC=1")
}
// Reduce Pulse Latency
if e.ReducePulseLatency {
log.Println("Reduce PulseAudio latency")
ctx.EnvVars = append(ctx.EnvVars, "PULSE_LATENCY_MSEC=60")
}
//Restart Pulse
if e.RestartPulse {
log.Println("Restart PulseAudio")
// Discard output; we don't care. Only that it worked or not.
_, err := exe.QuickExe("pulseaudio", "--kill")
if err != nil {
return nil, err
}
time.Sleep(1 * time.Second)
_, err = exe.QuickExe("pulseaudio", "--start")
if err != nil {
return nil, err
}
}
// Restore Resolution: Save
if e.RestoreResolution {
log.Println("Save Resolution")
xrandr, err := opt.ExePath("xrandr")
if err != nil {
//TODO: maybe don't return here and just emit a warning
return nil, err
}
displays, err := opt.DisplayResolutions(xrandr)
if err != nil {
return nil, err
}
ctx.Displays = displays
}
if e.useSandbox() {
command, arguments = e.addFirejailCli()
}
// Single Core
if e.SingleCore {
log.Println("Single Core")
if command == "" {
command = "taskset"
} else {
arguments = append(arguments, "taskset")
}
arguments = append(arguments, []string{"-c", "0", e.WineExe}...)
} else {
if command == "" {
command = e.WineExe
} else {
arguments = append(arguments, e.WineExe)
}
}
if e.VirtualDesktop != "" {
log.Printf("Enable Virtual Desktop (%s)", e.VirtualDesktop)
arguments = append(
arguments,
"explorer",
fmt.Sprintf("/desktop=%s", e.VirtualDesktop),
)
}
arguments = append(arguments, e.RunExe)
var overrides string
// Wine DLL overrides; do user overrides before any required ones
if e.WineDllOverrides != "" {
log.Println("Wine DLL Overrides")
overrides = e.WineDllOverrides
}
// Ensure the wine prefix dir exists before doing DXVK or VKD3D
if !e.PrefixExists() && (e.DxvkVersion != "" || e.Vkd3dVersion != "") {
log.Println("Generate Wine Prefix")
err = exe.GenPrefix(e.WineExe, e.WinePrefix, e.WineArch)
if err != nil {
return nil, err
}
}
has64 := e.WineArch == "win64" && e.wineCan64()
// DXVK
if e.DxvkVersion != "" {
log.Printf("Enable DXVK v%s", e.DxvkVersion)
// Is it a valid version?
valid, offline := e.ValidDllVersion(cacheDir, dll.DxvkKind, dll.DxvkReleaseURL, e.DxvkVersion)
if valid || offline {
// Enable the DXVK DLLs
err = dll.EnableAll(has64, cacheDir, dll.DxvkKind, e.WineArch, e.WinePrefix, e.DxvkVersion)
if err != nil {
return nil, err
}
// Inject DLL overrides
if overrides != "" {
overrides += ";"
}
overrides += "d3d9,d3d10,d3d10core,d3d11=native"
e.SysEnvVars = append(e.SysEnvVars, "WINE_LARGE_ADDRESS_AWARE=1")
} else {
return nil, fmt.Errorf("invalid DXVK version: %s", e.DxvkVersion)
}
} else if !strings.Contains(e.WineExe, "lutris-GE-Proton") {
// Disable the DXVK DLLs
err = dll.DisableAll(has64, cacheDir, dll.DxvkKind, e.WineArch, e.WinePrefix, e.DxvkVersion)
if err != nil {
return nil, err
}
}
// VKD3D
if e.Vkd3dVersion != "" {
log.Printf("Enable VKD3D v%s", e.Vkd3dVersion)
// Is it a valid version?
valid, offline := e.ValidDllVersion(cacheDir, dll.Vkd3dKind, dll.Vkd3dReleaseURL, e.Vkd3dVersion)
if valid || offline {
// Enable the VKD3D DLLs
err = dll.EnableAll(has64, cacheDir, dll.Vkd3dKind, e.WineArch, e.WinePrefix, e.Vkd3dVersion)
if err != nil {
return nil, err
}
// Inject DLL overrides
if overrides != "" {
overrides += ";"
}
overrides += "d3d12=native"
} else {
return nil, fmt.Errorf("invalid VKD3D version: %s", e.Vkd3dVersion)
}
} else if !strings.Contains(e.WineExe, "Proton-") {
// Disable the VKD3D DLLs
err = dll.DisableAll(has64, cacheDir, dll.Vkd3dKind, e.WineArch, e.WinePrefix, e.Vkd3dVersion)
if err != nil {
return nil, err
}
}
// Actually apply any overrides
if overrides != "" {
ctx.EnvVars = append(ctx.EnvVars, fmt.Sprintf("WINEDLLOVERRIDES=%s", overrides))
}
if e.useSandbox() {
arguments = append(arguments, "--")
}
ctx.Command = command
ctx.Args = arguments
return ctx, nil
}
func (e *WineEnv) PreRunCtx() (*exe.ExeCtx, error) {
if e.RunPre != "" {
ctx, err := e.ctx()
if err != nil {
return nil, err
}
runPre := strings.Split(e.RunPre, " ")
ctx.Command = runPre[0]
ctx.Args = append(ctx.Args, runPre[1:]...)
ctx.EnvVars = append(ctx.EnvVars, fmt.Sprintf("WINE=%s", e.WineExe))
ctx.Title = fmt.Sprintf("PreRun: %s", e.RunPre)
return ctx, nil
}
return nil, nil
}
func (e *WineEnv) PostRunCtx() (*exe.ExeCtx, error) {
if e.RunPost != "" {
ctx, err := e.ctx()
if err != nil {
return nil, err
}
runPost := strings.Split(e.RunPost, " ")
ctx.Command = runPost[0]
ctx.Args = append(ctx.Args, runPost[1:]...)
ctx.EnvVars = append(ctx.EnvVars, fmt.Sprintf("WINE=%s", e.WineExe))
ctx.Title = fmt.Sprintf("PostRun: %s", e.RunPost)
return ctx, nil
}
return nil, nil
}
func (e *WineEnv) InstallCtx() (*exe.ExeCtx, error) {
var command string
var arguments []string
ctx, err := e.ctx()
if err != nil {
return nil, err
}
ctx.WorkDir = e.InstallWorkDir
if e.useSandbox() {
command, arguments = e.addFirejailCli()
arguments = append(arguments, e.WineExe)
} else {
command = e.WineExe
}
arguments = append(arguments, e.InstallExe)
arguments = append(arguments, e.InstallArgs...)
if e.GogSilentInstall {
arguments = append(arguments, []string{"/dir=c:\\game", "/verysilent", "/sp-", "/suppressmsgboxes"}...)
}
if e.useSandbox() {
arguments = append(arguments, "--")
}
ctx.Command = command
ctx.Args = arguments
ctx.Title = fmt.Sprintf("Install \"%s\" (%s)", e.Name, e.Date)
return ctx, nil
}
func (e *WineEnv) ExeCtx(cacheDir string, skipInstall, skipRun bool) ([]*exe.ExeCtx, error) {
var err error
var installCtx *exe.ExeCtx
var runCtx *exe.ExeCtx
if !skipInstall && e.ShouldInstall() {
installCtx, err = e.InstallCtx()
if err != nil {
return nil, err
}
}
preCtx, err := e.PreRunCtx()
if err != nil {
return nil, err
}
if !skipRun {
runCtx, err = e.RunCtx(cacheDir)
if err != nil {
return nil, err
}
}
postCtx, err := e.PostRunCtx()
if err != nil {
return nil, err
}
return []*exe.ExeCtx{installCtx, preCtx, runCtx, postCtx}, nil
}
func (e *WineEnv) ExpectedExists() bool {
return dll.IsFile(e.InstallExpected) || dll.IsSymlink(e.InstallExpected)
}
func (e *WineEnv) IsInstalled() bool {
if e.InstallExe != "" && e.RunExe == "" {
return false
}
return e.ExpectedExists()
}
// RenderString is for when you need to one-off render a string.
func (e *WineEnv) RenderString(s string) (string, error) {
b, err := render(e, s)
return string(b), err
}
// Save writes the WineEnv to disk as a toml-formatted cfg file.
func (e *WineEnv) Save(name string, noUnchanged bool) error {
t, err := e.AsToml(noUnchanged)
if err != nil {
return err
}
c := e.CfgPath(name)
f, err := os.Create(c)
if err != nil {
return err
}
w := bufio.NewWriter(f)
defer w.Flush()
_, err = w.WriteString(t)
if err != nil {
return err
}
return nil
}
func (e *WineEnv) SetDefaults(c *cfg.WemConfig) error {
slug, err := e.Slug()
if err != nil {
return err
}
if e.WineExe == "" {
wineExe, err := opt.ExePath("wine")
if err != nil {
return errors.New("a wine executable was not found; please ensure wine and winetricks are installed before using WEM")
}
e.WineExe = wineExe
}
if e.Date == "" {
e.Date = time.Now().Format("2006-01-02 15:04:05")
}
if e.WineArch == "" {
e.WineArch = "win64"
}
if e.WinePrefix == "" {
e.WinePrefix = filepath.Join(c.WineEnvDir, slug)
}
winetricks, err := opt.ExePath("winetricks")
if err != nil {
winetricks = ""
}
if e.WinetricksPath == "" {
e.WinetricksPath = winetricks
}
return nil
}
func (e *WineEnv) ShouldInstall() bool {
return e.InstallExe != "" && !e.IsInstalled()
}
// Slug is a safe version of the env Name, consisting only of alphanumerics.
func (e *WineEnv) Slug() (string, error) {
slug, err := Slugify(e.Name)
if err != nil {
return "", err
}
return slug, nil
}
func Slugify(name string) (string, error) {
reg, err := regexp.Compile(`[^a-zA-Z0-9\-]+`)
if err != nil {
return "", err
}
return reg.ReplaceAllString(name, ""), nil
}
func (e *WineEnv) runTitle() string {
if e.Date == "" {
return fmt.Sprintf("Run: %s", e.Name)
} else {
return fmt.Sprintf("Run: %s (%s)", e.Name, e.Date)
}
}
func (e *WineEnv) ValidDllVersion(cacheDir, kind, url, version string) (bool, bool) {
ov := e.offlineValidate(cacheDir, kind, version)
if ov {
return false, true
}
// Request a list of versions from the API, check against those
vers, err := opt.GetVersions(url)
if err != nil {
if strings.Contains(err.Error(), "API rate limit") {
log.Printf("%s validation: "+err.Error(), strings.ToUpper(kind))
// Return true here because there's no way to actually validate it.
// We don't want to block on being ratelimited..
return true, false
} else {
log.Printf("There was an error getting available %s versions!", strings.ToUpper(kind))
return false, false
}
}
for _, v := range vers {
if v == version || v == "v"+version {
return true, false
}
}
return false, false
}
func (e *WineEnv) addFirejailCli() (string, []string) {
log.Println("Enable Firejail Sandbox")
var c string
var a []string
firejail, err := opt.ExePath("firejail")
if err == nil {
c = firejail
a = append(a,
fmt.Sprintf("--name='%s'", e.Name),
"--deterministic-exit-code",
"--deterministic-shutdown",
)
if e.SandboxBlacklist != nil {
for _, path := range e.SandboxBlacklist {
a = append(a, fmt.Sprintf("--blacklist=%s", path))
}
}
if e.SandboxWhitelist != nil {
for _, path := range e.SandboxWhitelist {
a = append(a, fmt.Sprintf("--whitelist=%s", path))
}
}
if e.Sandbox || e.SandboxCpu > 0 {
a = append(a, fmt.Sprintf("--cpu=%d", e.SandboxCpu))
}
if e.Sandbox || e.SandboxDns != "" {
a = append(a, fmt.Sprintf("--dns=%s", e.SandboxDns))
}
if e.Sandbox || e.SandboxIpcNamespace {
a = append(a, "--ipc-namespace")
}
if e.Sandbox || e.SandboxMachineId {
a = append(a, "--machine-id")
}
if e.Sandbox || e.SandboxNetNone {
a = append(a, "--net=none")
}
if e.Sandbox || e.SandboxNoDbus {
a = append(a, "--nodbus")
}
if e.Sandbox || e.SandboxNoPrinters {
a = append(a, "--noprinters")
}
if e.Sandbox || e.SandboxNoU2f {
a = append(a, "--nou2f")
}
if e.Sandbox || e.SandboxPrivateCache {
a = append(a, "--private-cache")
}
if e.Sandbox || e.SandboxPrivateCwd {
a = append(a, "--private-cwd")
}
if e.Sandbox || e.SandboxPrivateTmp {
a = append(a, "--private-tmp")
}
} else {
log.Println("WARNING: a 'firejail' executable was not found, the sandbox feature can't be used")
}
return c, a
}
func (e *WineEnv) writeFirejailString(s *strings.Builder) {
fmt.Fprintf(s,
"firejail --name='%s' --deterministic-exit-code --deterministic-shutdown ",
e.Name,
)
if e.SandboxBlacklist != nil {
for _, path := range e.SandboxBlacklist {
fmt.Fprintf(s, "--blacklist=%s ", path)
}
}
if e.SandboxWhitelist != nil {
for _, path := range e.SandboxWhitelist {
fmt.Fprintf(s, "--whitelist=%s ", path)
}
}
if e.Sandbox || e.SandboxCpu > 0 {
fmt.Fprintf(s, "--cpu=%d ", e.SandboxCpu)
}
if e.Sandbox || e.SandboxDns != "" {
fmt.Fprintf(s, "--dns=%s ", e.SandboxDns)
}
if e.Sandbox || e.SandboxIpcNamespace {
s.WriteString("--ipc-namespace ")
}
if e.Sandbox || e.SandboxMachineId {
s.WriteString("--machine-id ")
}
if e.Sandbox || e.SandboxNetNone {
s.WriteString("--net=none ")
}
if e.Sandbox || e.SandboxNoDbus {
s.WriteString("--nodbus ")
}
if e.Sandbox || e.SandboxNoPrinters {
s.WriteString("--noprinters ")
}
if e.Sandbox || e.SandboxNoU2f {
s.WriteString("--nou2f ")
}
if e.Sandbox || e.SandboxPrivateCache {
s.WriteString("--private-cache ")
}
if e.Sandbox || e.SandboxPrivateCwd {
s.WriteString("--private-cwd ")
}
if e.Sandbox || e.SandboxPrivateTmp {
s.WriteString("--private-tmp ")
}
}
func (e *WineEnv) offlineValidate(cacheDir, filePrefix, version string) bool {
// First check that the requested version is already installed by one name or another
_, err := os.Stat(filepath.Join(cacheDir, fmt.Sprintf("%s-%s", filePrefix, version)))
if err == nil {
return true
}
_, err = os.Stat(filepath.Join(cacheDir, e.Vkd3dVersion))
return err == nil
}
func (e *WineEnv) useSandbox() bool {
return e.Sandbox ||
(len(e.SandboxBlacklist) > 0 || len(e.SandboxWhitelist) > 0 ||
e.SandboxIpcNamespace || e.SandboxMachineId || e.SandboxNetNone ||
e.SandboxNoDbus || e.SandboxNoPrinters || e.SandboxNoU2f ||
e.SandboxPrivateCache || e.SandboxPrivateCwd || e.SandboxPrivateTmp)
}
// Validate ensures that mandatory config values are set.
func (e *WineEnv) Validate() error {
var installProblem bool
var missing string
if e.InstallExe == "" {
log.Println("WARNING: No \"InstallExe\" value is configured!")
log.Println("WARNING: WEM won't know what to run unless you set this.")
installProblem = true
} else {
if !dll.IsFile(e.InstallExe) {
log.Println("WARNING: InstallExe is set to a value that isn't a real file!")
installProblem = true
}
}
if e.InstallExpected == "" {
log.Println("WARNING: No \"InstallExpected\" value is configured!")
log.Println("WARNING: WEM won't know if it should install or not unless you set this.")
} else {
if !dll.IsFile(e.InstallExpected) {
log.Println("WARNING: InstallExpected is set to a value that isn't a real file!")
}
}
if e.RunExe == "" && installProblem {
missing += "RunExe "
} else if e.RunExe == "" && !installProblem && !e.IsInstalled() {
log.Println("WARNING: No RunExe has been set, be sure to configure that after installing")
}
if e.WineArch == "" {
missing += "WineArch "
}
if e.WineExe == "" {
missing += "WineExe "
}
if e.WinePrefix == "" {
missing += "WinePrefix "
}
if e.WineArch == "win64" && !e.wineCan64() {
log.Println("WARNING: A 64-bit prefix has been selected, but no 64-bit wine is available!")
log.Println("WARNING: WEM is auto-adjusting the prefix to 32-bit, please update")
log.Println("WARNING: your config or make a 64-bit wine available on your system")
e.WineArch = "win32"
}
if missing != "" {
return fmt.Errorf("missing required config options: %s", missing)
}
return nil
}
func (e *WineEnv) wineCan(cap string) (bool, error) {
c := exec.Command(e.WineExe, "--version")
o, err := c.CombinedOutput()
if err != nil {
return false, err
}
output := strings.ToLower(string(o))
if strings.Contains(output, cap) {
return true, nil
}
if strings.Contains(output, "staging") {
return true, nil
}
// A very wild guess but... actual proton doesn't report in a meaningful way.
if strings.Contains(strings.ToLower(e.WineExe), "proton") {
return true, nil
}
return false, nil
}
func (e *WineEnv) wineCan64() bool {
path, err := opt.ExePath("wine64")
if err != nil {
return false
}
if path == "" {
return false
}
return true
}
func FromName(name string, render bool) (*WineEnv, error) {
s, err := Slugify(name)
if err != nil {
return nil, err
}
p := filepath.Join(os.Getenv("HOME"), ".config", "wem", fmt.Sprintf("%s.cfg", s))
env, err := fromPath(p, render)
if err != nil {
return nil, err
}
return env, nil
}
// Take a string that's a file path and read it like it's an env cfg file.
func fromPath(p string, render bool) (*WineEnv, error) {
e, err := ioutil.ReadFile(p)
if err != nil {
return nil, err
}
info := &Info{}
installOpts := &InstallOpts{}
runOpts := &RunOpts{}
wineOpts := &WineOpts{}
sysOpts := &SysOpts{}
toml.Unmarshal(e, info)
toml.Unmarshal(e, installOpts)
toml.Unmarshal(e, runOpts)
toml.Unmarshal(e, wineOpts)
toml.Unmarshal(e, sysOpts)
we := &WineEnv{
Info: info,
InstallOpts: installOpts,
RunOpts: runOpts,
WineOpts: wineOpts,
SysOpts: sysOpts,
}
if render {
b, err := renderCfg(e, we)
if err != nil {
return nil, err
}
info := &Info{}
installOpts := &InstallOpts{}
runOpts := &RunOpts{}
wineOpts := &WineOpts{}
sysOpts := &SysOpts{}
toml.Unmarshal(b, info)
toml.Unmarshal(b, installOpts)
toml.Unmarshal(b, runOpts)
toml.Unmarshal(b, wineOpts)
toml.Unmarshal(b, sysOpts)
we = &WineEnv{
Info: info,
InstallOpts: installOpts,
RunOpts: runOpts,
WineOpts: wineOpts,
SysOpts: sysOpts,
}
}
if we.Sandbox {
we.SandboxIpcNamespace = true
we.SandboxMachineId = true
we.SandboxNetNone = true
we.SandboxNoDbus = true
we.SandboxNoPrinters = true
we.SandboxNoU2f = true
we.SandboxPrivateCache = true
we.SandboxPrivateCwd = true
we.SandboxPrivateTmp = true
}
return we, nil
}
func renderCfg(tomlCfg []byte, e *WineEnv) ([]byte, error) {
return render(e, os.ExpandEnv(string(tomlCfg)))
}
func render(e *WineEnv, str string) ([]byte, error) {
var count int
var err error
var tpl *template.Template
for {
if count >= 100 {
return nil, errors.New("exceeded render count")
}
tpl, err = template.New("rendered").Parse(str)
if err != nil {
return nil, err
}
buf := new(bytes.Buffer)
err = tpl.Execute(buf, e)
if err != nil {
return nil, err
}
// Re-parse and re-execute the template to render all vars
s := buf.String()
hasDollarSign := strings.Contains(s, "$")
hasDoubleLeftBace := strings.Contains(s, "{{")
hasDoubleRightBace := strings.Contains(s, "}}")
if !hasDollarSign && (!hasDoubleLeftBace || !hasDoubleRightBace) {
return buf.Bytes(), nil
}
str = os.ExpandEnv(s)
count += 1
}
}