flamenco/internal/find_blender/find_blender.go
Sybren A. Stüvel 02fac6a4df Change Go package name from git.blender.org to projects.blender.org
Change the package base name of the Go code, from
`git.blender.org/flamenco` to `projects.blender.org/studio/flamenco`.

The old location, `git.blender.org`, has no longer been use since the
[migration to Gitea][1]. The new package names now reflect the actual
location where Flamenco is hosted.

[1]: https://code.blender.org/2023/02/new-blender-development-infrastructure/
2023-08-01 12:42:31 +02:00

153 lines
4.9 KiB
Go

package find_blender
// SPDX-License-Identifier: GPL-3.0-or-later
import (
"context"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/rs/zerolog/log"
"projects.blender.org/studio/flamenco/pkg/api"
"projects.blender.org/studio/flamenco/pkg/crosspath"
)
var (
ErrNotAvailable = errors.New("not available on this platform")
ErrAssociationNotFound = errors.New("no program is associated with .blend files")
ErrNotBlender = errors.New("not a Blender executable")
ErrTimedOut = errors.New("version check took too long")
)
// 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
}
// Find returns the path of a `blender` executable,
// If there is one associated with .blend files, and the current platform is
// supported to query those, that one is used. Otherwise $PATH is searched.
func Find(ctx context.Context) (CheckBlenderResult, error) {
return CheckBlender(ctx, "")
}
// 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("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 for us.
return checkBlenderAtPath(ctx, exename)
}
// Try to find exename on $PATH
fullPath, err := exec.LookPath(exename)
if err != nil {
return CheckBlenderResult{}, err
}
return getResultWithVersion(ctx, exename, fullPath, api.BlenderPathSourcePathEnvvar)
}
func checkBlenderAtPath(ctx context.Context, path string) (CheckBlenderResult, error) {
stat, err := os.Stat(path)
if err != nil {
return CheckBlenderResult{}, err
}
if !stat.IsDir() {
// Simple case, it's not a directory so let's just try and execute it.
return getResultWithVersion(ctx, path, path, api.BlenderPathSourceInputPath)
}
// Try appending the Blender executable name.
log.Debug().
Str("path", path).
Str("executable", blenderExeName).
Msg("blender finder: given path is directory, going to find Blender executable")
exepath := filepath.Join(path, blenderExeName)
return getResultWithVersion(ctx, path, exepath, api.BlenderPathSourceInputPath)
}
// 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()
switch {
case errors.Is(cmdCtx.Err(), context.DeadlineExceeded):
logger.Warn().Stringer("timeout", blenderVersionTimeout).Msg("command timed out")
return "", fmt.Errorf("%s: %w", commandline, ErrTimedOut)
case err != nil:
logger.Info().Err(err).Str("output", string(stdoutStderr)).Msg("error running command")
return "", err
}
version := string(stdoutStderr)
lines := strings.Split(version, "\n")
for idx := range lines {
line := strings.TrimSpace(lines[idx])
if strings.HasPrefix(line, "Blender ") {
return line, nil
}
}
return "", fmt.Errorf("%s: %w", commandline, ErrNotBlender)
}