flamenco/internal/find_blender/find_blender.go
Sybren A. Stüvel cb6a3a5a88 Manager: test error with errors.Is() instead of ==
It's just a better way to test errors.
2022-07-26 17:25:50 +02:00

114 lines
3.4 KiB
Go

package find_blender
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"context"
"errors"
"fmt"
"os/exec"
"strings"
"time"
"git.blender.org/flamenco/pkg/api"
"git.blender.org/flamenco/pkg/crosspath"
"github.com/rs/zerolog/log"
)
var ErrNotAvailable = errors.New("not available on this platform")
// blenderVersionTimeout is how long `blender --version` is allowed to take,
// before timing out. This can be much slower than expected, when loading
// Blender from shared storage on a not-so-fast NAS.
const blenderVersionTimeout = 10 * time.Second
type CheckBlenderResult struct {
Input string // What was the original 'exename' CheckBlender was told to find.
FoundLocation string
BlenderVersion string
Source api.BlenderPathSource
}
// FileAssociation returns the full path of a Blender executable, by inspecting file association with .blend files.
// `ErrNotAvailable` is returned if no "blender finder" is available for the current platform.
func FileAssociation() (string, error) {
// findBlender() is implemented in one of the platform-dependent files.
return fileAssociation()
}
func CheckBlender(ctx context.Context, exename string) (CheckBlenderResult, error) {
if exename == "" {
// exename is not given, see if we can use .blend file association.
fullPath, err := fileAssociation()
switch {
case errors.Is(err, ErrNotAvailable):
// Association finder not available, act as if "blender" was given as exename.
return CheckBlender(ctx, "blender")
case err != nil:
// Some other error occurred, better to report it.
return CheckBlenderResult{}, fmt.Errorf("error finding .blend file association: %w", err)
default:
// The full path was found, report the Blender version.
return getResultWithVersion(ctx, exename, fullPath, api.BlenderPathSourceFileAssociation)
}
}
if crosspath.Dir(exename) != "." {
// exename is some form of path, see if it works directly as executable.
return getResultWithVersion(ctx, exename, exename, api.BlenderPathSourceInputPath)
}
// Try to find exename on $PATH
fullPath, err := exec.LookPath(exename)
if err != nil {
return CheckBlenderResult{}, err
}
return getResultWithVersion(ctx, exename, fullPath, api.BlenderPathSourcePathEnvvar)
}
// getResultWithVersion tries to run the command to get Blender's version.
// The result is returned as a `CheckBlenderResult` struct.
func getResultWithVersion(
ctx context.Context,
input,
commandline string,
source api.BlenderPathSource,
) (CheckBlenderResult, error) {
result := CheckBlenderResult{
Input: input,
FoundLocation: commandline,
Source: source,
}
version, err := getBlenderVersion(ctx, commandline)
if err != nil {
return result, err
}
result.BlenderVersion = version
return result, nil
}
func getBlenderVersion(ctx context.Context, commandline string) (string, error) {
logger := log.With().Str("commandline", commandline).Logger()
// Make sure that command execution doesn't hang indefinitely.
cmdCtx, cmdCtxCancel := context.WithTimeout(ctx, blenderVersionTimeout)
defer cmdCtxCancel()
cmd := exec.CommandContext(cmdCtx, commandline, "--version")
stdoutStderr, err := cmd.CombinedOutput()
if err != nil {
logger.Info().Err(err).Str("output", string(stdoutStderr)).Msg("error running command")
return "", err
}
version := string(stdoutStderr)
lines := strings.SplitN(version, "\n", 2)
if len(lines) > 0 {
version = lines[0]
}
return strings.TrimSpace(version), nil
}