Sybren A. Stüvel
02fac6a4df
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/
153 lines
4.9 KiB
Go
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)
|
|
}
|