diff --git a/cmd/publisher/commands/deploy.go b/cmd/publisher/commands/deploy.go index 4de14e650..74c27f562 100644 --- a/cmd/publisher/commands/deploy.go +++ b/cmd/publisher/commands/deploy.go @@ -66,7 +66,9 @@ func (cmd *DeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContext) stateStore.Account.Name, stateStore.ConfigName, stateStore.SaveName) - publisher, err := publish.NewFromState(stateStore, events.NewCliEmitter(os.Stderr, ctx.Logger), ctx.Logger) + + rExecutable := util.Path{} + publisher, err := publish.NewFromState(stateStore, rExecutable, events.NewCliEmitter(os.Stderr, ctx.Logger), ctx.Logger) if err != nil { return err } diff --git a/cmd/publisher/commands/redeploy.go b/cmd/publisher/commands/redeploy.go index 8b544fac9..dbaf2b2cc 100644 --- a/cmd/publisher/commands/redeploy.go +++ b/cmd/publisher/commands/redeploy.go @@ -52,7 +52,8 @@ func (cmd *RedeployCmd) Run(args *cli_types.CommonArgs, ctx *cli_types.CLIContex stateStore.Account.Name, stateStore.ConfigName) - publisher, err := publish.NewFromState(stateStore, events.NewCliEmitter(os.Stderr, ctx.Logger), ctx.Logger) + rExecutable := util.Path{} + publisher, err := publish.NewFromState(stateStore, rExecutable, events.NewCliEmitter(os.Stderr, ctx.Logger), ctx.Logger) if err != nil { return err } diff --git a/extensions/vscode/src/@types/positron.d.ts b/extensions/vscode/src/@types/positron.d.ts new file mode 100644 index 000000000..5b4c640f6 --- /dev/null +++ b/extensions/vscode/src/@types/positron.d.ts @@ -0,0 +1,74 @@ +// Copyright (C) 2024 by Posit Software, PBC. + +// This is the portion of the Positron API definition +// used by Publisher, here until it is published. + +declare module "positron" { + export interface PositronApi { + version: string; + runtime: runtime; + } + + /** + * LanguageRuntimeMetadata contains information about a language runtime that is known + * before the runtime is started. + */ + export interface LanguageRuntimeMetadata { + /** The path to the runtime. */ + runtimePath: string; + + /** A unique identifier for this runtime; takes the form of a GUID */ + runtimeId: string; + + /** + * The fully qualified name of the runtime displayed to the user; e.g. "R 4.2 (64-bit)". + * Should be unique across languages. + */ + runtimeName: string; + + /** + * A language specific runtime name displayed to the user; e.g. "4.2 (64-bit)". + * Should be unique within a single language. + */ + runtimeShortName: string; + + /** The version of the runtime itself (e.g. kernel or extension version) as a string; e.g. "0.1" */ + runtimeVersion: string; + + /** The runtime's source or origin; e.g. PyEnv, System, Homebrew, Conda, etc. */ + runtimeSource: string; + + /** The free-form, user-friendly name of the language this runtime can execute; e.g. "R" */ + languageName: string; + + /** + * The Visual Studio Code Language ID of the language this runtime can execute; e.g. "r" + * + * See here for a list of known language IDs: + * https://code.visualstudio.com/docs/languages/identifiers#_known-language-identifiers + */ + languageId: string; + + /** The version of the language; e.g. "4.2" */ + languageVersion: string; + + /** The Base64-encoded icon SVG for the language. */ + base64EncodedIconSvg: string | undefined; + + /** Whether the runtime should start up automatically or wait until explicitly requested */ + // startupBehavior: LanguageRuntimeStartupBehavior; + + /** Where sessions will be located; used as a hint to control session restoration */ + // sessionLocation: LanguageRuntimeSessionLocation; + + /** + * Extra data supplied by the runtime provider; not read by Positron but supplied + * when creating a new session from the metadata. + */ + extraRuntimeData: any; + } + + export interface runtime { + getPreferredRuntime(languageId: string): Thenable; + } +} diff --git a/extensions/vscode/src/api/resources/Configurations.ts b/extensions/vscode/src/api/resources/Configurations.ts index 151df4789..a1f7176bf 100644 --- a/extensions/vscode/src/api/resources/Configurations.ts +++ b/extensions/vscode/src/api/resources/Configurations.ts @@ -81,12 +81,14 @@ export class Configurations { inspect( dir: string, python?: string, + r?: string, params?: { entrypoint?: string; recursive?: boolean }, ) { return this.client.post( "/inspect", { python, + r, }, { params: { diff --git a/extensions/vscode/src/api/resources/ContentRecords.ts b/extensions/vscode/src/api/resources/ContentRecords.ts index 5b576bfbc..d76cf82b7 100644 --- a/extensions/vscode/src/api/resources/ContentRecords.ts +++ b/extensions/vscode/src/api/resources/ContentRecords.ts @@ -75,12 +75,14 @@ export class ContentRecords { insecure: boolean, dir: string, secrets?: Record, + r?: string, ) { const data = { account: accountName, config: configName, secrets: secrets, insecure: insecure, + r: r, }; const encodedTarget = encodeURIComponent(targetName); return this.client.post<{ localId: string }>( diff --git a/extensions/vscode/src/api/resources/Packages.ts b/extensions/vscode/src/api/resources/Packages.ts index 0fd88bea0..d15e18cb2 100644 --- a/extensions/vscode/src/api/resources/Packages.ts +++ b/extensions/vscode/src/api/resources/Packages.ts @@ -62,10 +62,10 @@ export class Packages { // 200 - success // 400 - bad request // 500 - internal server error - createRRequirementsFile(dir: string, saveName?: string) { + createRRequirementsFile(dir: string, saveName?: string, r?: string) { return this.client.post( "packages/r/scan", - { saveName }, + { saveName, r }, { params: { dir } }, ); } diff --git a/extensions/vscode/src/entrypointTracker.ts b/extensions/vscode/src/entrypointTracker.ts index bc3c56e18..37efe6239 100644 --- a/extensions/vscode/src/entrypointTracker.ts +++ b/extensions/vscode/src/entrypointTracker.ts @@ -14,7 +14,10 @@ import { Utils as uriUtils } from "vscode-uri"; import { useApi } from "src/api"; import { Contexts } from "src/constants"; -import { getPythonInterpreterPath } from "src/utils/config"; +import { + getPythonInterpreterPath, + getRInterpreterPath, +} from "src/utils/vscode"; import { isActiveDocument, relativeDir } from "src/utils/files"; import { hasKnownContentType } from "src/utils/inspect"; import { getSummaryStringFromError } from "src/utils/errors"; @@ -43,8 +46,9 @@ async function isDocumentEntrypoint( try { const api = await useApi(); const python = await getPythonInterpreterPath(); + const r = await getRInterpreterPath(); - const response = await api.configurations.inspect(dir, python, { + const response = await api.configurations.inspect(dir, python, r, { entrypoint: uriUtils.basename(document.uri), }); diff --git a/extensions/vscode/src/multiStepInputs/newDeployment.ts b/extensions/vscode/src/multiStepInputs/newDeployment.ts index 3842812b1..d80bf812e 100644 --- a/extensions/vscode/src/multiStepInputs/newDeployment.ts +++ b/extensions/vscode/src/multiStepInputs/newDeployment.ts @@ -32,7 +32,10 @@ import { ContentType, FileAction, } from "src/api"; -import { getPythonInterpreterPath } from "src/utils/config"; +import { + getPythonInterpreterPath, + getRInterpreterPath, +} from "src/utils/vscode"; import { getMessageFromError, getSummaryStringFromError, @@ -121,12 +124,14 @@ export async function newDeployment( try { const python = await getPythonInterpreterPath(); + const r = await getRInterpreterPath(); const relEntryPointDir = path.dirname(relEntryPoint); const relEntryPointFile = path.basename(relEntryPoint); const inspectResponse = await api.configurations.inspect( relEntryPointDir, python, + r, { entrypoint: relEntryPointFile, }, diff --git a/extensions/vscode/src/multiStepInputs/selectNewOrExistingConfig.ts b/extensions/vscode/src/multiStepInputs/selectNewOrExistingConfig.ts index 2997a10d9..f6a1134f2 100644 --- a/extensions/vscode/src/multiStepInputs/selectNewOrExistingConfig.ts +++ b/extensions/vscode/src/multiStepInputs/selectNewOrExistingConfig.ts @@ -23,7 +23,10 @@ import { isConfigurationError, useApi, } from "src/api"; -import { getPythonInterpreterPath } from "src/utils/config"; +import { + getPythonInterpreterPath, + getRInterpreterPath, +} from "src/utils/vscode"; import { getSummaryStringFromError } from "src/utils/errors"; import { MultiStepInput, @@ -175,9 +178,12 @@ export async function selectNewOrExistingConfig( const getConfigurationInspections = async () => { try { const python = await getPythonInterpreterPath(); + const r = await getRInterpreterPath(); + const inspectResponse = await api.configurations.inspect( activeDeployment.projectDir, python, + r, ); inspectionResults = filterInspectionResultsToType( inspectResponse.data, diff --git a/extensions/vscode/src/utils/config.ts b/extensions/vscode/src/utils/config.ts deleted file mode 100644 index 1691932de..000000000 --- a/extensions/vscode/src/utils/config.ts +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (C) 2024 by Posit Software, PBC. - -import { Uri, commands, workspace } from "vscode"; -import { fileExists, isDir } from "./files"; -import { substituteVariables } from "./variables"; - -export async function getPythonInterpreterPath(): Promise { - const workspaceFolder = workspace.workspaceFolders?.[0]; - if (workspaceFolder === undefined) { - return undefined; - } - let configuredPython: string | undefined; - try { - configuredPython = await commands.executeCommand( - "python.interpreterPath", - { workspaceFolder: workspaceFolder }, - ); - } catch { - // Ignore - } - if (configuredPython === undefined) { - return undefined; - } - let python = substituteVariables(configuredPython, true); - const pythonUri = Uri.file(python); - - if (await isDir(pythonUri)) { - // Configured python can be a directory such as a virtual environment. - const names = [ - "bin/python", - "bin/python3", - "Scripts/python.exe", - "Scripts/python3.exe", - ]; - for (const name of names) { - const candidate = Uri.joinPath(pythonUri, name); - if (await fileExists(candidate)) { - python = candidate.fsPath; - } - } - } - console.log("Python interpreter path:", python); - return python; -} diff --git a/extensions/vscode/src/utils/throttle.ts b/extensions/vscode/src/utils/throttle.ts index a7f61821c..46acb8a33 100644 --- a/extensions/vscode/src/utils/throttle.ts +++ b/extensions/vscode/src/utils/throttle.ts @@ -40,3 +40,7 @@ export const throttleWithLastPending = async ( } } }; + +export function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/extensions/vscode/src/utils/vscode.ts b/extensions/vscode/src/utils/vscode.ts new file mode 100644 index 000000000..104923147 --- /dev/null +++ b/extensions/vscode/src/utils/vscode.ts @@ -0,0 +1,109 @@ +// Copyright (C) 2024 by Posit Software, PBC. + +import { Uri, commands, workspace } from "vscode"; +import { fileExists, isDir } from "./files"; +import { delay } from "./throttle"; +import { substituteVariables } from "./variables"; +import { LanguageRuntimeMetadata, PositronApi } from "positron"; + +export async function getPythonInterpreterPath(): Promise { + const workspaceFolder = workspace.workspaceFolders?.[0]; + if (workspaceFolder === undefined) { + return undefined; + } + let configuredPython: string | undefined; + try { + configuredPython = await commands.executeCommand( + "python.interpreterPath", + { workspaceFolder: workspaceFolder }, + ); + } catch (error: unknown) { + console.error( + "getPythonInterpreterPath was unable to execute command. Error = ", + error, + ); + } + if (configuredPython === undefined) { + return undefined; + } + let python = substituteVariables(configuredPython, true); + const pythonUri = Uri.file(python); + + if (await isDir(pythonUri)) { + // Configured python can be a directory such as a virtual environment. + const names = [ + "bin/python", + "bin/python3", + "Scripts/python.exe", + "Scripts/python3.exe", + ]; + for (const name of names) { + const candidate = Uri.joinPath(pythonUri, name); + if (await fileExists(candidate)) { + python = candidate.fsPath; + } + } + } + console.log("Python interpreter path:", python); + return python; +} + +declare global { + function acquirePositronApi(): PositronApi; +} + +let positronApi: PositronApi | null | undefined; + +function getPositronApi(): PositronApi | null { + if (positronApi === undefined) { + try { + positronApi = acquirePositronApi(); + } catch { + positronApi = null; + } + } + return positronApi; +} + +export async function getRInterpreterPath(): Promise { + const api = getPositronApi(); + + if (api) { + let runtime: LanguageRuntimeMetadata | undefined; + + // Small number of retries, because getPreferredRuntime + // has its own internal retry logic. + const retries = 3; + const retryInterval = 1000; + + for (let i = 0; i < retries + 1; i++) { + try { + runtime = await api.runtime.getPreferredRuntime("r"); + break; + } catch (error: unknown) { + // Delay and retry + console.error( + "getPreferredRuntime returned an error; retrying. ", + error, + ); + await delay(retryInterval); + } + } + + if (runtime) { + const interpreter = runtime.runtimePath; + console.log("Using selected R interpreter", interpreter); + return interpreter; + } else { + console.log( + "Using default R interpreter because getPreferredRuntime did not return one", + ); + } + } + // We don't know the interpreter path. + // The backend will run R from PATH. + console.log( + "Using default R interpreter because the Positron API is not available", + ); + return undefined; +} diff --git a/extensions/vscode/src/views/homeView.ts b/extensions/vscode/src/views/homeView.ts index 5f4351b8a..1f69a284a 100644 --- a/extensions/vscode/src/views/homeView.ts +++ b/extensions/vscode/src/views/homeView.ts @@ -38,7 +38,7 @@ import { EnvironmentConfig, } from "src/api"; import { EventStream } from "src/events"; -import { getPythonInterpreterPath } from "../utils/config"; +import { getPythonInterpreterPath, getRInterpreterPath } from "../utils/vscode"; import { getSummaryStringFromError } from "src/utils/errors"; import { getNonce } from "src/utils/getNonce"; import { getUri } from "src/utils/getUri"; @@ -197,6 +197,8 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable { ) { try { const api = await useApi(); + const r = await getRInterpreterPath(); + const response = await api.contentRecords.publish( deploymentName, credentialName, @@ -204,6 +206,7 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable { !extensionSettings.verifyCertificates(), // insecure = !verifyCertificates projectDir, secrets, + r, ); deployProject(response.data.localId, this.stream); } catch (error: unknown) { @@ -721,9 +724,12 @@ export class HomeViewProvider implements WebviewViewProvider, Disposable { Views.HomeView, async () => { const api = await useApi(); + const r = await getRInterpreterPath(); + return await api.packages.createRRequirementsFile( activeConfiguration.projectDir, relPathPackageFile, + r, ); }, ); diff --git a/internal/initialize/initialize.go b/internal/initialize/initialize.go index fcb61371a..636010d68 100644 --- a/internal/initialize/initialize.go +++ b/internal/initialize/initialize.go @@ -59,7 +59,7 @@ func inspectProject(base util.AbsolutePath, python util.Path, rExecutable util.P } cfg.Python = pyConfig } - needR, err := requiresR(cfg, base, rExecutable) + needR, err := requiresR(cfg, base) if err != nil { return nil, err } @@ -92,12 +92,7 @@ func requiresPython(cfg *config.Config, base util.AbsolutePath) (bool, error) { return exists, nil } -func requiresR(cfg *config.Config, base util.AbsolutePath, rExecutable util.Path) (bool, error) { - if rExecutable.String() != "" { - // If user provided R on the command line, - // then configure R for the project. - return true, nil - } +func requiresR(cfg *config.Config, base util.AbsolutePath) (bool, error) { if cfg.R != nil { // InferType returned an R configuration for us to fill in. return true, nil @@ -166,7 +161,7 @@ func normalizeConfig( cfg.Python = pyConfig cfg.Files = append(cfg.Files, fmt.Sprint("/", cfg.Python.PackageFile)) } - needR, err := requiresR(cfg, base, rExecutable) + needR, err := requiresR(cfg, base) if err != nil { log.Debug("Error while determining R as a requirement", "error", err.Error()) return err diff --git a/internal/inspect/r.go b/internal/inspect/r.go index e262ade26..9b11e8eb7 100644 --- a/internal/inspect/r.go +++ b/internal/inspect/r.go @@ -5,8 +5,10 @@ package inspect import ( "bytes" "encoding/json" + "errors" "fmt" "io/fs" + "os" "os/exec" "regexp" "strings" @@ -14,6 +16,7 @@ import ( "github.com/posit-dev/publisher/internal/config" "github.com/posit-dev/publisher/internal/executor" "github.com/posit-dev/publisher/internal/logging" + "github.com/posit-dev/publisher/internal/types" "github.com/posit-dev/publisher/internal/util" ) @@ -163,21 +166,30 @@ func (i *defaultRInspector) getRExecutable() (string, error) { if exists { return i.rExecutable.String(), nil } - return "", fmt.Errorf( + noExecErr := fmt.Errorf( "cannot find the specified R executable %s: %w", i.rExecutable, fs.ErrNotExist) - } else { - // Use whatever is on PATH - path, err := i.pathLooker.LookPath("R") + return "", types.NewAgentError(types.ErrorRExecNotFound, noExecErr, nil) + } + // Find the executable on PATH + var path string + var err error + + i.log.Info("Looking for R on PATH", "PATH", os.Getenv("PATH")) + path, err = i.pathLooker.LookPath("R") + if err == nil { + // Ensure the R is actually runnable. + err = i.validateRExecutable(path) if err == nil { - // Ensure the R is actually runnable. - err = i.validateRExecutable(path) - } - if err != nil { - return "", err + return path, nil } - return path, nil } + + if errors.Is(err, exec.ErrNotFound) { + return "", types.NewAgentError(types.ErrorRExecNotFound, err, nil) + } + + return "", err } var rVersionRE = regexp.MustCompile(`^R version (\d+\.\d+\.\d+)`) diff --git a/internal/inspect/r_test.go b/internal/inspect/r_test.go index f35aa5a00..0032b8c97 100644 --- a/internal/inspect/r_test.go +++ b/internal/inspect/r_test.go @@ -4,13 +4,13 @@ package inspect import ( "errors" - "io/fs" "os/exec" "runtime" "testing" "github.com/posit-dev/publisher/internal/executor/executortest" "github.com/posit-dev/publisher/internal/logging" + "github.com/posit-dev/publisher/internal/types" "github.com/posit-dev/publisher/internal/util" "github.com/posit-dev/publisher/internal/util/utiltest" "github.com/spf13/afero" @@ -319,9 +319,12 @@ func (s *RSuite) TestGetRExecutableSpecifiedRNotFound() { i := NewRInspector(s.cwd, util.NewPath("/some/R", nil), log) inspector := i.(*defaultRInspector) - executable, err := inspector.getRExecutable() - s.ErrorIs(err, fs.ErrNotExist) + + aerr, yes := types.IsAgentError(err) + s.Equal(yes, true) + s.Equal(aerr.Code, types.ErrorRExecNotFound) + s.Contains(aerr.Message, "Cannot find the specified R executable /some/R: file does not exist.") s.Equal("", executable) } diff --git a/internal/publish/publish.go b/internal/publish/publish.go index 3ccd5eb3e..cd7a76cab 100644 --- a/internal/publish/publish.go +++ b/internal/publish/publish.go @@ -63,7 +63,7 @@ type publishDeployedFailureData struct { DirectURL string `mapstructure:"url"` } -func NewFromState(s *state.State, emitter events.Emitter, log logging.Logger) (Publisher, error) { +func NewFromState(s *state.State, rExecutable util.Path, emitter events.Emitter, log logging.Logger) (Publisher, error) { if s.LocalID != "" { data := baseEventData{ LocalID: s.LocalID, @@ -79,7 +79,7 @@ func NewFromState(s *state.State, emitter events.Emitter, log logging.Logger) (P State: s, log: log, emitter: emitter, - rPackageMapper: renv.NewPackageMapper(s.Dir, util.Path{}), + rPackageMapper: renv.NewPackageMapper(s.Dir, rExecutable), }, nil } diff --git a/internal/publish/publish_test.go b/internal/publish/publish_test.go index ee0d248ee..038cafbe6 100644 --- a/internal/publish/publish_test.go +++ b/internal/publish/publish_test.go @@ -115,7 +115,7 @@ func (s *PublishSuite) SetupTest() { func (s *PublishSuite) TestNewFromState() { stateStore := state.Empty() - publisher, err := NewFromState(stateStore, events.NewNullEmitter(), logging.New()) + publisher, err := NewFromState(stateStore, util.Path{}, events.NewNullEmitter(), logging.New()) s.NoError(err) s.Equal(stateStore, publisher.(*defaultPublisher).State) } diff --git a/internal/services/api/patch_deployment_test.go b/internal/services/api/patch_deployment_test.go index b7a41900c..3ec9d7ac6 100644 --- a/internal/services/api/patch_deployment_test.go +++ b/internal/services/api/patch_deployment_test.go @@ -14,8 +14,6 @@ import ( "github.com/posit-dev/publisher/internal/config" "github.com/posit-dev/publisher/internal/deployment" "github.com/posit-dev/publisher/internal/logging" - "github.com/posit-dev/publisher/internal/publish" - "github.com/posit-dev/publisher/internal/state" "github.com/posit-dev/publisher/internal/types" "github.com/posit-dev/publisher/internal/util" "github.com/posit-dev/publisher/internal/util/utiltest" @@ -33,9 +31,6 @@ func TestPatchDeploymentHandlerFuncSuite(t *testing.T) { } func (s *PatchDeploymentHandlerFuncSuite) SetupTest() { - stateFactory = state.New - publisherFactory = publish.NewFromState - afs := afero.NewMemMapFs() cwd, err := util.Getwd(afs) s.Nil(err) diff --git a/internal/services/api/post_deployment.go b/internal/services/api/post_deployment.go index e689c384b..72a2ec73c 100644 --- a/internal/services/api/post_deployment.go +++ b/internal/services/api/post_deployment.go @@ -23,6 +23,7 @@ type PostDeploymentRequestBody struct { ConfigName string `json:"config"` Secrets map[string]string `json:"secrets,omitempty"` Insecure bool `json:"insecure"` + R string `json:"r"` } type PostDeploymentsReponse struct { @@ -114,7 +115,8 @@ func PostDeploymentHandlerFunc( log := log.WithArgs("local_id", localID) newState.LocalID = localID - publisher, err := publisherFactory(newState, emitter, log) + rExecutable := util.NewPath(b.R, nil) + publisher, err := publisherFactory(newState, rExecutable, emitter, log) log.Debug("New publisher derived from state", "account", b.AccountName, "config", b.ConfigName) if err != nil { InternalError(w, req, log, err) diff --git a/internal/services/api/post_deployment_test.go b/internal/services/api/post_deployment_test.go index 3e6804b4c..420480b82 100644 --- a/internal/services/api/post_deployment_test.go +++ b/internal/services/api/post_deployment_test.go @@ -73,7 +73,7 @@ func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentHandlerFunc() { publisher := &mockPublisher{} publisher.On("PublishDirectory", mock.Anything).Return(nil) - publisherFactory = func(*state.State, events.Emitter, logging.Logger) (publish.Publisher, error) { + publisherFactory = func(*state.State, util.Path, events.Emitter, logging.Logger) (publish.Publisher, error) { return publisher, nil } stateFactory = func( @@ -205,7 +205,7 @@ func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentHandlerFuncPublishErr testErr := errors.New("test error from PublishDirectory") publisher := &mockPublisher{} publisher.On("PublishDirectory", mock.Anything).Return(testErr) - publisherFactory = func(*state.State, events.Emitter, logging.Logger) (publish.Publisher, error) { + publisherFactory = func(*state.State, util.Path, events.Emitter, logging.Logger) (publish.Publisher, error) { return publisher, nil } @@ -241,7 +241,7 @@ func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentSubdir() { publisher := &mockPublisher{} publisher.On("PublishDirectory", mock.Anything).Return(nil) - publisherFactory = func(*state.State, events.Emitter, logging.Logger) (publish.Publisher, error) { + publisherFactory = func(*state.State, util.Path, events.Emitter, logging.Logger) (publish.Publisher, error) { return publisher, nil } stateFactory = func( @@ -290,7 +290,7 @@ func (s *PostDeploymentHandlerFuncSuite) TestPostDeploymentHandlerFuncWithSecret publisher := &mockPublisher{} publisher.On("PublishDirectory", mock.Anything).Return(nil) - publisherFactory = func(*state.State, events.Emitter, logging.Logger) (publish.Publisher, error) { + publisherFactory = func(*state.State, util.Path, events.Emitter, logging.Logger) (publish.Publisher, error) { return publisher, nil } diff --git a/internal/services/api/post_inspect.go b/internal/services/api/post_inspect.go index 5212f6296..9fa6f0690 100644 --- a/internal/services/api/post_inspect.go +++ b/internal/services/api/post_inspect.go @@ -20,6 +20,7 @@ import ( type postInspectRequestBody struct { Python string `json:"python"` + R string `json:"r"` } type postInspectResponseBody struct { @@ -74,9 +75,12 @@ func PostInspectHandlerFunc(base util.AbsolutePath, log logging.Logger) http.Han return } pythonPath := util.NewPath(b.Python, nil) + rPath := util.NewPath(b.R, nil) + response := []postInspectResponseBody{} log.Debug("Python path to be used for inspection", "path", pythonPath) + log.Debug("R path to be used for inspection", "path", rPath) if req.URL.Query().Get("recursive") == "true" { log.Debug("Recursive inspection intent found") @@ -108,7 +112,7 @@ func PostInspectHandlerFunc(base util.AbsolutePath, log logging.Logger) http.Han } entrypoint := req.URL.Query().Get("entrypoint") entrypointPath := util.NewRelativePath(entrypoint, base.Fs()) - configs, err := initialize.GetPossibleConfigs(path, pythonPath, util.Path{}, entrypointPath, log) + configs, err := initialize.GetPossibleConfigs(path, pythonPath, rPath, entrypointPath, log) if err != nil { return err } @@ -146,7 +150,7 @@ func PostInspectHandlerFunc(base util.AbsolutePath, log logging.Logger) http.Han // Response already returned by getEntrypointPath return } - configs, err := initialize.GetPossibleConfigs(projectDir, pythonPath, util.Path{}, entrypointPath, log) + configs, err := initialize.GetPossibleConfigs(projectDir, pythonPath, rPath, entrypointPath, log) if err != nil { if aerr, ok := types.IsAgentErrorOf(err, types.ErrorPythonExecNotFound); ok { apiErr := types.APIErrorPythonExecNotFoundFromAgentError(*aerr) diff --git a/internal/services/api/post_packages_r_scan.go b/internal/services/api/post_packages_r_scan.go index 9bf847074..8f7b536e0 100644 --- a/internal/services/api/post_packages_r_scan.go +++ b/internal/services/api/post_packages_r_scan.go @@ -15,6 +15,7 @@ import ( ) type PostPackagesRScanRequest struct { + R string `json:"r"` SaveName string `json:"saveName"` } @@ -58,7 +59,8 @@ func (h *PostPackagesRScanHandler) ServeHTTP(w http.ResponseWriter, req *http.Re return } lockfileAbsPath := projectDir.Join(path.String()) - inspector := rInspectorFactory(projectDir, util.Path{}, h.log) + rPath := util.NewPath(b.R, nil) + inspector := rInspectorFactory(projectDir, rPath, h.log) err = inspector.CreateLockfile(lockfileAbsPath) if err != nil { InternalError(w, req, h.log, err) diff --git a/internal/services/api/post_packages_r_scan_test.go b/internal/services/api/post_packages_r_scan_test.go index 4a1692e14..dccebe20e 100644 --- a/internal/services/api/post_packages_r_scan_test.go +++ b/internal/services/api/post_packages_r_scan_test.go @@ -40,7 +40,7 @@ func (s *PostPackagesRScanSuite) TestNewPostPackagesRScanHandler() { func (s *PostPackagesRScanSuite) TestServeHTTP() { rec := httptest.NewRecorder() - body := strings.NewReader(`{"saveName":""}`) + body := strings.NewReader(`{"saveName":"", "r": "/opt/R/bin/R"}`) req, err := http.NewRequest("POST", "/api/packages/r/scan", body) s.NoError(err) @@ -52,7 +52,10 @@ func (s *PostPackagesRScanSuite) TestServeHTTP() { log := logging.New() h := NewPostPackagesRScanHandler(base, log) - rInspectorFactory = func(util.AbsolutePath, util.Path, logging.Logger) inspect.RInspector { + rInspectorFactory = func(baseDir util.AbsolutePath, rExec util.Path, log logging.Logger) inspect.RInspector { + s.Equal(base, baseDir) + s.Equal(util.NewPath("/opt/R/bin/R", nil), rExec) + i := inspect.NewMockRInspector() i.On("CreateLockfile", destPath).Return(nil) return i diff --git a/internal/types/error.go b/internal/types/error.go index 0154ad6e9..0af9943e4 100644 --- a/internal/types/error.go +++ b/internal/types/error.go @@ -29,6 +29,7 @@ const ( ErrorTomlValidationError ErrorCode = "tomlValidationError" ErrorTomlUnknownError ErrorCode = "tomlUnknownError" ErrorPythonExecNotFound ErrorCode = "pythonExecNotFound" + ErrorRExecNotFound ErrorCode = "rExecNotFound" ) type EventableError interface {