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 }