Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,17 @@ export async function getInstalledPackagesDiagnostics(
const scriptPath = installedCheckScript();
try {
traceInfo('Running installed packages checker: ', interpreter, scriptPath, doc.uri.fsPath);
const envCopy = { ...process.env, VSCODE_MISSING_PGK_SEVERITY: `${getMissingPackageSeverity(doc)}` };
// --- Start Positron ---
// const envCopy = { ...process.env, VSCODE_MISSING_PGK_SEVERITY: `${getMissingPackageSeverity(doc)}` };
// The script imports libraries that ship with the extension. Do not
// write their bytecode into the extension folder, which can be inside
// a signed or read-only app bundle.
const envCopy = {
...process.env,
VSCODE_MISSING_PGK_SEVERITY: `${getMissingPackageSeverity(doc)}`,
PYTHONDONTWRITEBYTECODE: '1',
};
// --- End Positron ---
const result = await plainExec(interpreter.path, [scriptPath, doc.uri.fsPath], {
env: envCopy,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,17 @@
import { inject, injectable } from 'inversify';
import * as path from 'path';
import { ITerminalManager } from '../../common/application/types';
import { pathExists } from '../../common/platform/fs-paths';
// --- Start Positron ---
// import { pathExists } from '../../common/platform/fs-paths';
import { copyFile, pathExists } from '../../common/platform/fs-paths';
// --- End Positron ---
import { _SCRIPTS_DIR } from '../../common/process/internal/scripts/constants';
import { identifyShellFromShellPath } from '../../common/terminal/shellDetectors/baseShellDetector';
import { ITerminalHelper, TerminalShellType } from '../../common/terminal/types';
import { Resource } from '../../common/types';
// --- Start Positron ---
// import { Resource } from '../../common/types';
import { IExtensionContext, Resource } from '../../common/types';
// --- End Positron ---
import { waitForCondition } from '../../common/utils/async';
import { cache } from '../../common/utils/decorators';
import { StopWatch } from '../../common/utils/stopWatch';
Expand All @@ -34,11 +40,19 @@ const ShellIntegrationShells = [
export class TerminalDeactivateService implements ITerminalDeactivateService {
private readonly envVarScript = path.join(_SCRIPTS_DIR, 'printEnvVariablesToFile.py');

// --- Start Positron ---
// constructor(
// @inject(ITerminalManager) private readonly terminalManager: ITerminalManager,
// @inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
// @inject(ITerminalHelper) private readonly terminalHelper: ITerminalHelper,
// ) {}
constructor(
@inject(ITerminalManager) private readonly terminalManager: ITerminalManager,
@inject(IInterpreterService) private readonly interpreterService: IInterpreterService,
@inject(ITerminalHelper) private readonly terminalHelper: ITerminalHelper,
@inject(IExtensionContext) private readonly context: IExtensionContext,
) {}
// --- End Positron ---

@cache(-1, true)
public async initializeScriptParams(shell: string): Promise<void> {
Expand All @@ -47,6 +61,16 @@ export class TerminalDeactivateService implements ITerminalDeactivateService {
return;
}
const shellType = identifyShellFromShellPath(shell);
// --- Start Positron ---
// The script reads envVars.txt from its own folder, and the extension
// folder can be inside a signed or read-only app bundle. Copy the
// script to global storage so that both files are written there.
const scriptName = this.getScriptName(shellType);
await copyFile(
path.join(_SCRIPTS_DIR, 'deactivate', this.getShellFolderName(shellType), scriptName),
path.join(location, scriptName),
);
// --- End Positron ---
const terminal = this.terminalManager.createTerminal({
name: `Python ${shellType} Deactivate`,
shellPath: shell,
Expand Down Expand Up @@ -81,9 +105,24 @@ export class TerminalDeactivateService implements ITerminalDeactivateService {
if (!ShellIntegrationShells.includes(shellType)) {
return undefined;
}
return path.join(_SCRIPTS_DIR, 'deactivate', this.getShellFolderName(shellType));
// --- Start Positron ---
// return path.join(_SCRIPTS_DIR, 'deactivate', this.getShellFolderName(shellType));
return path.join(this.context.globalStorageUri.fsPath, 'deactivate', this.getShellFolderName(shellType));
// --- End Positron ---
}

// --- Start Positron ---
private getScriptName(shellType: TerminalShellType): string {
switch (shellType) {
case TerminalShellType.powershell:
case TerminalShellType.powershellCore:
return 'deactivate.ps1';
default:
return 'deactivate';
}
}
// --- End Positron ---

private getShellFolderName(shellType: TerminalShellType): string {
switch (shellType) {
case TerminalShellType.powershell:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ suite('Install check diagnostics tests', () => {
configMock.verifyAll();
});

// --- Start Positron ---
test('Does not write bytecode into the extension folder', async () => {
configMock.setup((c) => c.get<string>('missingPackage.severity', 'Hint')).returns(() => 'Hint');
let dontWriteBytecode: string | undefined;
plainExecStub.callsFake((_cmd: string, _args: string[], options: SpawnOptions) => {
dontWriteBytecode = options.env?.PYTHONDONTWRITEBYTECODE;
return { stdout: '', stderr: '' };
});
const someFile = getSomeRequirementFile();
await getInstalledPackagesDiagnostics(interpreterService.object, someFile.object);

assert.deepStrictEqual(dontWriteBytecode, '1');
});
// --- End Positron ---

[
['Error', '0'],
['Warning', '1'],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
/*---------------------------------------------------------------------------------------------
* Copyright (C) 2026 Posit Software, PBC. All rights reserved.
* Licensed under the Elastic License 2.0. See LICENSE.txt for license information.
*--------------------------------------------------------------------------------------------*/

import { assert } from 'chai';
import * as path from 'path';
import * as sinon from 'sinon';
import * as typemoq from 'typemoq';
import { Terminal, Uri } from 'vscode';
import { ITerminalManager } from '../../../client/common/application/types';
import * as fsPaths from '../../../client/common/platform/fs-paths';
import { _SCRIPTS_DIR } from '../../../client/common/process/internal/scripts/constants';
import { ITerminalHelper } from '../../../client/common/terminal/types';
import { IExtensionContext } from '../../../client/common/types';
import { IInterpreterService } from '../../../client/interpreter/contracts';
import { PythonEnvType } from '../../../client/pythonEnvironments/base/info';
import { PythonEnvironment } from '../../../client/pythonEnvironments/info';
import { TerminalDeactivateService } from '../../../client/terminals/envCollectionActivation/deactivateService';

suite('Terminal deactivate service', () => {
const storageDir = path.join('global', 'storage');
let copyFileStub: sinon.SinonStub;
let terminalManager: typemoq.IMock<ITerminalManager>;
let interpreterService: typemoq.IMock<IInterpreterService>;
let service: TerminalDeactivateService;

setup(() => {
copyFileStub = sinon.stub(fsPaths, 'copyFile').resolves();
sinon.stub(fsPaths, 'pathExists').resolves(true);

terminalManager = typemoq.Mock.ofType<ITerminalManager>();
const terminal = typemoq.Mock.ofType<Terminal>();
terminalManager.setup((t) => t.createTerminal(typemoq.It.isAny())).returns(() => terminal.object);

interpreterService = typemoq.Mock.ofType<IInterpreterService>();
interpreterService.setup((i) => i.getInterpreters()).returns(() => []);
interpreterService
.setup((i) => i.getActiveInterpreter(typemoq.It.isAny()))
.returns(() => Promise.resolve({ type: PythonEnvType.Virtual } as PythonEnvironment));

const terminalHelper = typemoq.Mock.ofType<ITerminalHelper>();
terminalHelper
.setup((t) => t.buildCommandForTerminal(typemoq.It.isAny(), typemoq.It.isAny(), typemoq.It.isAny()))
.returns(() => 'command');

const context = typemoq.Mock.ofType<IExtensionContext>();
context.setup((c) => c.globalStorageUri).returns(() => Uri.file(storageDir));

service = new TerminalDeactivateService(
terminalManager.object,
interpreterService.object,
terminalHelper.object,
context.object,
);
});

teardown(() => {
sinon.restore();
});

test('Script location is in global storage, not the extension folder', async () => {
const location = await service.getScriptLocation('/bin/zsh', undefined);

assert.deepStrictEqual(location, path.join(Uri.file(storageDir).fsPath, 'deactivate', 'zsh'));
});

[
['/bin/zsh', 'zsh', 'deactivate'],
['pwsh', 'powershell', 'deactivate.ps1'],
].forEach(([shell, folder, script]) => {
test(`Copies the ${folder} script to global storage and runs the terminal there`, async () => {
await service.initializeScriptParams(shell);

const location = path.join(Uri.file(storageDir).fsPath, 'deactivate', folder);
assert.deepStrictEqual(copyFileStub.args, [
[path.join(_SCRIPTS_DIR, 'deactivate', folder, script), path.join(location, script)],
]);
terminalManager.verify(
(t) => t.createTerminal(typemoq.It.is((options) => options.cwd === location)),
typemoq.Times.once(),
);
});
});
});
Loading