初始化上传
Some checks failed
Run mypy_primer on push / Run mypy_primer on push (push) Has been cancelled
Validation / Typecheck (push) Has been cancelled
Validation / Style (push) Has been cancelled
Validation / Test macos-latest (push) Has been cancelled
Validation / Test ubuntu-latest (push) Has been cancelled
Validation / Test windows-latest (push) Has been cancelled
Validation / Build (push) Has been cancelled
Validation / Required (push) Has been cancelled

This commit is contained in:
2026-07-24 17:08:39 +08:00
commit e9e4693333
7516 changed files with 768821 additions and 0 deletions

View File

@@ -0,0 +1,323 @@
trigger:
branches:
include:
- refs/tags/*
resources:
repositories:
- repository: MicroBuildTemplate
type: git
name: 1ESPipelineTemplates/MicroBuildTemplate
ref: refs/tags/release
variables:
- name: TeamName
value: Pyright
- name: VSIX_NAME
value: vscode-pyright.vsix
- name: ARTIFACT_NAME_VSIX
value: vsix
- name: PACKAGE_NAME
value: pyright.tgz
- name: ARTIFACT_NAME_PACKAGE
value: pyright
- name: AZURE_ARTIFACTS_FEED
value: 'https://devdiv.pkgs.visualstudio.com/DevDiv/_packaging/Pylance_PublicPackages/npm/registry/'
extends:
template: azure-pipelines/MicroBuild.1ES.Official.Publish.yml@MicroBuildTemplate
parameters:
sdl:
codeSignValidation:
enabled: true
additionalTargetsGlobPattern: -|**/gdn-vscode-pyright.vsix/**
codeql:
compiled:
enabled: false
justificationForDisabling: 'Running a scan on the Pyright-Build azure-pipelines.yml'
sourceAnalysisPool: VSEngSS-MicroBuild2022-1ES
pool:
name: AzurePipelines-EO
image: 1ESPT-Ubuntu22.04
os: linux
customBuildTags:
- ES365AIMigrationTooling
stages:
- stage: BuildVsix
jobs:
- job: build_vsix
displayName: Build VSIX
timeoutInMinutes: 60
pool:
name: VSEngSS-MicroBuild2022-1ES # use windows for codesigning to make things easier https://dev.azure.com/devdiv/DevDiv/_wiki/wikis/DevDiv.wiki/650/MicroBuild-Signing
os: windows
templateContext:
mb:
signing:
enabled: true
signType: 'real'
signWithProd: true
outputs:
- output: pipelineArtifact
displayName: 'publish vsix artifact'
targetPath: 'build_output'
artifactName: $(ARTIFACT_NAME_VSIX)
steps:
- checkout: self
fetchDepth: 1
clean: true
submodules: true
fetchTags: false
persistCredentials: True
- task: NodeTool@0
displayName: Use Node 24.15.0
inputs:
versionSpec: 24.15.0
- template: /build/templates/npmAuthenticate.yml@self
- task: CmdLine@2
displayName: npm install
inputs:
script: npm run install:all
- task: CmdLine@2
displayName: Package VSIX
inputs:
script: |
npm run package
workingDirectory: packages/vscode-pyright
- task: PowerShell@2
inputs:
targetType: 'inline'
script: 'Move-Item -Path "pyright-*.vsix" -Destination "$(VSIX_NAME)"'
workingDirectory: packages/vscode-pyright
displayName: 'Move VSIX file'
- task: CopyFiles@2
displayName: 'Copy vsix to: build_output'
inputs:
SourceFolder: packages/vscode-pyright
Contents: '$(VSIX_NAME)'
TargetFolder: build_output
- script: |
npm install -g @vscode/vsce
displayName: 'Install vsce and dependencies'
- script: npx vsce generate-manifest -i $(VSIX_NAME) -o extension.manifest
displayName: 'Generate extension manifest'
workingDirectory: packages/vscode-pyright
- task: NuGetToolInstaller@1
displayName: 'Install NuGet'
- task: NuGetCommand@2
inputs:
command: 'restore'
restoreSolution: '$(Build.SourcesDirectory)/packages/vscode-pyright/packages.config'
restoreDirectory: '$(Build.SourcesDirectory)/packages/vscode-pyright/packages'
- task: MSBuild@1
displayName: 'Sign binaries'
inputs:
solution: 'packages/vscode-pyright/sign.proj'
msbuildArguments: '/verbosity:diagnostic /p:SignType=real'
- task: PowerShell@2
displayName: 'Compare extension.manifest and extension.signature.p7s'
inputs:
targetType: 'inline'
script: |
$manifestPath = "$(Build.SourcesDirectory)\packages\vscode-pyright\extension.manifest"
$signaturePath = "$(Build.SourcesDirectory)\packages\vscode-pyright\extension.signature.p7s"
$compareResult = Compare-Object (Get-Content $manifestPath) (Get-Content $signaturePath)
if ($compareResult -eq $null) {
Write-Error "Files are identical. Failing the build."
exit 1
} else {
Write-Output "Files are different."
}
- task: CopyFiles@2
displayName: 'Copy extension.manifest'
inputs:
SourceFolder: 'packages/vscode-pyright'
Contents: 'extension.manifest'
TargetFolder: build_output
- task: CopyFiles@2
displayName: 'Copy extension.signature.p7s'
inputs:
SourceFolder: 'packages/vscode-pyright'
Contents: 'extension.signature.p7s'
TargetFolder: build_output
- stage: BuildNpm
dependsOn: # Blank to allow running in parallel with BuildVsix
jobs:
- job: build_npm
displayName: Build NPM
templateContext:
outputs:
- output: pipelineArtifact
targetPath: '$(Build.ArtifactStagingDirectory)/${{ variables.ARTIFACT_NAME_PACKAGE }}/${{ variables.PACKAGE_NAME }}'
sbomBuildDropPath: '$(Build.ArtifactStagingDirectory)/${{ variables.ARTIFACT_NAME_PACKAGE }}'
artifactName: ${{ variables.ARTIFACT_NAME_PACKAGE }}
steps:
- checkout: self
fetchDepth: 1
clean: true
submodules: true
fetchTags: false
persistCredentials: True
- task: NodeTool@0
displayName: Use Node 24.15.0
inputs:
versionSpec: 24.15.0
- template: /build/templates/npmAuthenticate.yml@self
- task: CmdLine@2
displayName: npm install
inputs:
script: npm run install:all
- script: npm pack
displayName: npm pack
workingDirectory: packages/pyright
- script: mv pyright-*.tgz ${{ variables.PACKAGE_NAME }}
displayName: Remove version from package name
workingDirectory: packages/pyright
# CredScan does not understand scanning a single file. So, it will default to scanning the whole folder that your output is pointing to.
# We don't want it to scan the microbuild plugins, which is installed in the agent.stagedirectory by default.
- task: CopyFiles@2
displayName: 'Copy ${{ variables.PACKAGE_NAME }} to subfolder'
inputs:
SourceFolder: packages/pyright
Contents: '${{ variables.PACKAGE_NAME }}'
TargetFolder: '$(Build.ArtifactStagingDirectory)/${{ variables.ARTIFACT_NAME_PACKAGE }}'
- stage: CreateRelease
dependsOn:
- BuildVsix
- BuildNpm
jobs:
- job: create_release
displayName: Create GitHub Release
templateContext:
type: releaseJob
isProduction: true
inputs:
- input: pipelineArtifact
artifactName: $(ARTIFACT_NAME_VSIX)
targetPath: $(Pipeline.Workspace)/$(ARTIFACT_NAME_VSIX)
- input: pipelineArtifact
artifactName: $(ARTIFACT_NAME_PACKAGE)
targetPath: $(Pipeline.Workspace)/$(ARTIFACT_NAME_PACKAGE)
steps:
- checkout: none
# Next step fails the build if https://github.com/microsoft/pyright/issues/10350 repros.
- script: |
mkdir pyrightTest
cd pyrightTest
yarn
yarn add --dev $(Pipeline.Workspace)/$(ARTIFACT_NAME_PACKAGE)/$(PACKAGE_NAME)
node_modules/.bin/pyright --version
displayName: Validate npm package
- task: GitHubRelease@1 #https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/github-release-v1?view=azure-pipelines
displayName: 'Create GitHub Release'
inputs:
gitHubConnection: 'Github-Pylance' # The name of the GitHub service connection
repositoryName: 'microsoft/pyright' # The name of your GitHub repository
action: 'create'
isDraft: true
isPreRelease: false
addChangeLog: true
title: 'Published $(Build.SourceBranchName)'
assets: |
$(Pipeline.Workspace)/$(ARTIFACT_NAME_VSIX)/$(VSIX_NAME)
$(Pipeline.Workspace)/$(ARTIFACT_NAME_PACKAGE)/$(PACKAGE_NAME)
- stage: WaitForValidation
dependsOn:
- CreateRelease
jobs:
- job: wait_for_validation
displayName: Wait for manual validation
pool: server
templateContext:
mb:
publish:
enabled: false
steps:
- task: ManualValidation@0
timeoutInMinutes: 120 # task times out in 2 hours
inputs:
notifyUsers: '[DevDiv]\Python.Language Server,eric@traut.com'
instructions: 'In the next 2 hours please test the latest draft release of Pyright, then Publish the release in GitHub.'
onTimeout: 'reject' # require sign-off
- stage: PublishToMarketplace
dependsOn:
- WaitForValidation
jobs:
- job: publish_extension
displayName: Publish extension to marketplace
steps:
- checkout: none
- task: NodeTool@0
inputs:
versionSpec: 24.15.0
- task: DownloadPipelineArtifact@2
displayName: 'Download Artifacts from Validation Job'
inputs:
buildType: 'current'
artifactName: '$(ARTIFACT_NAME_VSIX)'
targetPath: '$(System.ArtifactsDirectory)'
# https://code.visualstudio.com/api/working-with-extensions/publishing-extension
# Install dependencies and VS Code Extension Manager (vsce >= v2.26.1 needed)
- script: |
npm install -g @vscode/vsce
displayName: 'Install vsce and dependencies'
# https://code.visualstudio.com/api/working-with-extensions/publishing-extension#get-a-personal-access-token
# Publish to Marketplace
# see. stackoverflow.com/collectives/ci-cd/articles/76873787/publish-azure-devops-extensions-using-azure-workload-identity
# az rest -u https://app.vssps.visualstudio.com/_apis/profile/profiles/me --resource 499b84ac-1321-427f-aa17-267ca6975798
- task: AzureCLI@2
displayName: 'Publishing with Managed Identity'
inputs:
azureSubscription: PyrightPublishPipelineSecureConnectionWithManagedIdentity
scriptType: 'pscore'
scriptLocation: 'inlineScript'
inlineScript: |
$aadToken = az account get-access-token --query accessToken --resource 499b84ac-1321-427f-aa17-267ca6975798 -o tsv
vsce publish --pat $aadToken --packagePath $(System.ArtifactsDirectory)/$(VSIX_NAME) --noVerify --manifestPath $(System.ArtifactsDirectory)/extension.manifest --signaturePath $(System.ArtifactsDirectory)/extension.signature.p7s
- stage: PublishToNpm
dependsOn:
- WaitForValidation
- PublishToMarketplace
jobs:
- job: publish_package
displayName: Publish package to NPM
pool:
name: VSEngSS-MicroBuild2022-1ES # This pool is required to have the certs needed to publish using ESRP.
os: windows
image: server2022-microbuildVS2022-1es
templateContext:
type: releaseJob
isProduction: true
inputs:
- input: pipelineArtifact
artifactName: $(ARTIFACT_NAME_PACKAGE)
targetPath: $(Build.StagingDirectory)/dist
steps:
- template: MicroBuild.Publish.yml@MicroBuildTemplate
parameters:
intent: PackageDistribution
contentType: npm
contentSource: Folder
folderLocation: $(Build.StagingDirectory)/dist
waitForReleaseCompletion: true
owners: rchiodo@microsoft.com
approvers: grwheele@microsoft.com

View File

@@ -0,0 +1,62 @@
trigger: none
resources:
repositories:
- repository: MicroBuildTemplate
type: git
name: 1ESPipelineTemplates/MicroBuildTemplate
ref: refs/tags/release
variables:
- name: SigningType
value: 'real'
- name: TeamName
value: Pyright
extends:
template: azure-pipelines/MicroBuild.1ES.Official.yml@MicroBuildTemplate
parameters:
sdl:
sourceAnalysisPool: VSEngSS-MicroBuild2022-1ES
pool:
name: AzurePipelines-EO
demands:
- ImageOverride -equals 1ESPT-Ubuntu22.04
os: Linux
customBuildTags:
- ES365AIMigrationTooling
stages:
- stage: stage
jobs:
- job: build
displayName: Build VSIX
timeoutInMinutes: 720
templateContext:
outputs:
- output: pipelineArtifact
displayName: 'publish vsix artifact'
targetPath: build_output
artifactName: vsix-win
steps:
- checkout: self
clean: true
submodules: true
fetchTags: true
persistCredentials: True
- task: NodeTool@0
displayName: Use Node 24.15.0
inputs:
versionSpec: 24.15.0
- task: CmdLine@2
displayName: npm install
inputs:
script: npm run install:all
- task: CmdLine@2
displayName: Package VSIX
inputs:
script: |
npm run package
workingDirectory: packages/vscode-pyright
- task: CopyFiles@2
displayName: 'Copy vsix to: build_output'
inputs:
SourceFolder: packages/vscode-pyright
Contents: '*.vsix'
TargetFolder: build_output

View File

@@ -0,0 +1,206 @@
# generateUnicodeTables.py
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
#
# Generates the content of unicode.ts based on the official Unicode
# character database.
import sys
import urllib.request
from io import TextIOWrapper
class Character:
def __init__(self, code: int, category: str, *, end: int | None = None):
self.code = code
self.category = category
self.hasSurrogate = code > 0xFFFF
if self.hasSurrogate:
unicodeChar = chr(code)
utf16 = unicodeChar.encode("utf-16")
rawHex = utf16.hex()
hex = rawHex[4:]
self.highSurrogate = int(hex[2:4] + hex[0:2], base=16)
self.lowSurrogate = int(hex[6:8] + hex[4:6], base=16)
class CharacterRange:
def __init__(self, start: Character, end: Character):
self.start = start
self.end = end
def downloadUnicodeData(unicodeVersion: str) -> str:
url = f"https://www.unicode.org/Public/{unicodeVersion}.0/ucd/UnicodeData.txt"
(path, _) = urllib.request.urlretrieve(url)
return path
def parseFile(filePath: str) -> list[Character]:
with open(filePath, "r") as reader:
lines = reader.readlines()
chars: list[Character] = []
for i in range(len(lines)):
line = lines[i]
splitOnSemicolon = line.split(";")
charCode = int(splitOnSemicolon[0], base=16)
category = splitOnSemicolon[2]
if splitOnSemicolon[1].endswith(", First>"):
# Legacy range syntax
# D800;<Non Private Use High Surrogate, First>;Cs;0;L;;;;;N;;;;;
# DB7F;<Non Private Use High Surrogate, Last>;Cs;0;L;;;;;N;;;;;
nextLine = lines[i + 1]
nextSplitOnSemicolon = nextLine.split(";")
nextCharCode = int(nextSplitOnSemicolon[0], base=16)
for ord in range(charCode, nextCharCode + 1):
chars.append(Character(ord, category))
elif splitOnSemicolon[1].endswith(", Last>"):
continue
else:
chars.append(Character(charCode, category))
return chars
# Given a collection of characters, returns a list of ranges of contiguous
# characters. Contiguous means that the character codes are sequential with
# no gaps and the characters all have the same category. For character codes
# greater than 0xFFFF, contiguous means that the high surrogate is the same
# and the low surrogate values are sequential with no gaps. So, two character
# codes might be sequential numerically but have different high surrogates,
# and therefore would not be members of the same range.
def getSurrogateRanges(chars: list[Character]) -> list[CharacterRange]:
surrogateRanges: list[CharacterRange] = []
consecutiveRangeStartChar: Character | None = None
previousChar: Character | None = None
for char in chars:
if not consecutiveRangeStartChar:
consecutiveRangeStartChar = char
if previousChar:
if not previousChar.hasSurrogate and not char.hasSurrogate:
if (
char.code == previousChar.code + 1
and char.category == previousChar.category
):
pass
elif not previousChar.hasSurrogate and char.hasSurrogate:
consecutiveRangeStartChar = char
else:
if (
char.highSurrogate == previousChar.highSurrogate
and char.lowSurrogate == previousChar.lowSurrogate + 1
and char.category == previousChar.category
):
pass
else:
surrogateRanges.append(
CharacterRange(consecutiveRangeStartChar, previousChar)
)
consecutiveRangeStartChar = char
previousChar = char
return surrogateRanges
# Write out a table of all character codes within the specified category. These are
# the full hex character codes (Unicode code points) not surrogate values. Sequential
# ranges of character codes are written as arrays of two numbers (start and end) to
# save space.
def writeRangeTable(writer: TextIOWrapper, category: str, chars: list[Character]):
chars = [ch for ch in chars if ch.category == category]
writer.write("\n")
writer.write(f"export const unicode{category}: UnicodeRangeTable = [\n")
consecutiveRangeStartChar: Character | None = None
for i in range(len(chars)):
char = chars[i]
if not consecutiveRangeStartChar:
consecutiveRangeStartChar = char
if i + 1 >= len(chars) or chars[i + 1].code != char.code + 1:
if consecutiveRangeStartChar.code == char.code:
writer.write(f" 0x{consecutiveRangeStartChar.code:04x},\n")
else:
writer.write(f" [0x{consecutiveRangeStartChar.code:04x}, 0x{char.code:04x}],\n")
consecutiveRangeStartChar = None
writer.write("];\n")
# Write out a table of all characters within the specified category using their UTF-16
# values. Characters are grouped by high surrogate value. Sequential ranges of low
# surrogate values are written as arrays of two numbers (start and end) to save space.
def writeSurrogateRangeTable(
writer: TextIOWrapper, category: str, surrogateRanges: list[CharacterRange]
):
surrogateRanges = [r for r in surrogateRanges if r.start.category == category]
if len(surrogateRanges) == 0:
return
writer.write("\n")
writer.write(
f"export const unicode{category}Surrogate: UnicodeSurrogateRangeTable = {{\n"
)
previousCharRange: CharacterRange | None = None
for charRange in surrogateRanges:
if (
previousCharRange
and charRange.start.highSurrogate != previousCharRange.start.highSurrogate
):
writer.write(" ],\n")
previousCharRange = None
if not previousCharRange:
writer.write(f" 0x{charRange.start.highSurrogate:04x}: [\n")
previousCharRange = charRange
if charRange.start.lowSurrogate == charRange.end.lowSurrogate:
writer.write(f" 0x{charRange.start.lowSurrogate:04x}, // 0x{charRange.start.code:04X}\n")
else:
writer.write(
f" [0x{charRange.start.lowSurrogate:04x}, 0x{charRange.end.lowSurrogate:04x}], // 0x{charRange.start.code:04X}..0x{charRange.end.code:04X}\n"
)
writer.write(" ],\n")
writer.write("};\n")
unicodeVersion = "16.0" if len(sys.argv) <= 1 else sys.argv[1]
path = downloadUnicodeData(unicodeVersion)
chars = parseFile(path)
surrogateRanges = getSurrogateRanges(chars)
with open("packages/pyright-internal/src/parser/unicode.ts", "w") as writer:
writer.write(
f"""/*
* unicode.ts
* Copyright (c) Microsoft Corporation.
* Licensed under the MIT license.
*
* Tables that encode Unicode character codes for various Unicode-
* defined categories used in the Python spec.
*
* Generated by build/generateUnicodeTables.py from the UnicodeData.txt
* metadata file for Unicode {unicodeVersion}.
*/
export type UnicodeRange = [number, number] | number;
export type UnicodeRangeTable = UnicodeRange[];
export type UnicodeSurrogateRangeTable = {{ [surrogate: number]: UnicodeRange[] }};
"""
)
for category in ["Lu", "Ll", "Lt", "Lo", "Lm", "Nl", "Mn", "Mc", "Nd", "Pc"]:
writeRangeTable(writer, category, chars)
writeSurrogateRangeTable(writer, category, surrogateRanges)

83
build/lib/updateDeps.js Normal file
View File

@@ -0,0 +1,83 @@
/* eslint-disable @typescript-eslint/no-var-requires */
//@ts-check
const { promises: fsAsync } = require('fs');
const ncu = require('npm-check-updates');
const PQueue = require('p-queue').default;
const path = require('path');
const util = require('util');
const { glob } = require('glob');
const exec = util.promisify(require('child_process').exec);
/** @type {((path: string, options?: import('fs').RmOptions) => Promise<void>) | undefined} */
const node14rm = /** @type {any} */ (fsAsync).rm;
/** @type {(path: string, options?: { recursive?: boolean }) => Promise<void>} */
const legacyRmdir = /** @type {any} */ (fsAsync.rmdir);
/** @type {(path: string) => Promise<void>} */
async function rmdir(path) {
if (node14rm) {
// Avoid deprecation warning when on Node v14+, which have deprecated recursive rmdir in favor of rm.
return node14rm(path, { recursive: true, force: true });
}
return legacyRmdir(path, { recursive: true });
}
async function findPackages() {
const lernaFile = await fsAsync.readFile('lerna.json', 'utf-8');
/** @type {{ packages: string[] }} */
const lernaConfig = JSON.parse(lernaFile);
const matches = await Promise.all(lernaConfig.packages.map((pattern) => glob(pattern + '/package.json')));
return ['package.json'].concat(...matches);
}
const queue = new PQueue({ concurrency: 4 });
/** @type {(packageFile: string, transitive: boolean, reject?: string[]) => Promise<void>} */
async function updatePackage(packageFile, transitive, reject = undefined) {
packageFile = path.resolve(packageFile);
const packagePath = path.dirname(packageFile);
const packageName = path.basename(packagePath);
console.log(`${packageName}: updating with ncu`);
const updateResult = await ncu.run({
packageFile: packageFile,
target: 'minor',
upgrade: true,
reject: reject,
});
if (!transitive && Object.keys(/**@type {any}*/ (updateResult)).length === 0) {
// If nothing changed and we aren't updating transitive deps, don't run npm install.
return;
}
if (transitive) {
console.log(`${packageName}: removing package-lock.json and node_modules`);
await fsAsync.unlink(path.join(packagePath, 'package-lock.json'));
await rmdir(path.join(packagePath, 'node_modules'));
}
await queue.add(async () => {
console.log(`${packageName}: reinstalling package`);
await exec('npm install', {
cwd: packagePath,
env: {
...process.env,
SKIP_LERNA_BOOTSTRAP: 'yes',
},
});
});
}
/** @type {(transitive: boolean, reject?: string[]) => Promise<void>} */
async function updateAll(transitive, reject = undefined) {
const packageFiles = await findPackages();
await Promise.all(packageFiles.map((packageFile) => updatePackage(packageFile, transitive, reject)));
}
module.exports = {
updateAll,
};

125
build/lib/webpack.js Normal file
View File

@@ -0,0 +1,125 @@
const fs = require('fs');
const path = require('path');
const glob = require('glob');
const JSONC = require('jsonc-parser');
const assert = require('assert');
/**
* Builds a faked resource path for production source maps in webpack.
*
* @param {string} packageName The name of the package where webpack is running.
*/
function monorepoResourceNameMapper(packageName) {
/**@type {(info: {resourcePath: string}) => string} */
const mapper = (info) => {
const parts = [];
// Walk backwards looking for the monorepo
for (const part of info.resourcePath.split('/').reverse()) {
if (part === '..' || part === 'packages') {
break;
}
if (part === '.') {
parts.push(packageName);
break;
}
parts.push(part);
}
return parts.reverse().join('/');
};
return mapper;
}
/**
* Returns the list of node_modules folders for the entire monorepo.
*
* @param {string} workspaceRoot
*/
function managedPaths(workspaceRoot) {
const contents = fs.readFileSync(path.join(workspaceRoot, 'lerna.json'), 'utf-8');
/** @type {{ packages: string[] }} */
const data = JSON.parse(contents);
const patterns = data.packages;
const paths = [path.resolve(workspaceRoot, 'node_modules')];
paths.push(
...patterns
.flatMap((p) => glob.sync(p, { cwd: workspaceRoot }))
.map((p) => path.resolve(workspaceRoot, p, 'node_modules'))
);
return paths;
}
/**
* Builds a webpack caching config, given the calling module's __dirname and __filename.
* @param {string} dirname __dirname
* @param {string} filename __filename
* @param {string | undefined} name name of the webpack instance, if using multiple configs
*/
function cacheConfig(dirname, filename, name = undefined) {
// Temporarily disabled: caching breaks when switching branches,
// after typescript compilation errors, and so on.
if (true) {
return undefined;
}
return {
type: /** @type {'filesystem'} */ ('filesystem'),
cacheDirectory: path.resolve(dirname, '.webpack_cache'),
buildDependencies: {
config: [filename],
},
managedPaths: managedPaths(path.resolve(dirname, '..', '..')),
name,
};
}
/**
* Builds a webpack resolve alias configuration from a tsconfig.json file.
* This is an alternative to using tsconfig-paths-webpack-plugin, which
* has a number of unfixed bugs.
*
* https://github.com/dividab/tsconfig-paths/issues/143
* https://github.com/dividab/tsconfig-paths-webpack-plugin/issues/59
* https://github.com/dividab/tsconfig-paths-webpack-plugin/issues/60
* @param {string} tsconfigPath Path to tsconfig
*/
function tsconfigResolveAliases(tsconfigPath) {
tsconfigPath = path.resolve(tsconfigPath);
const tsconfigDir = path.dirname(tsconfigPath);
const tsconfig = JSONC.parse(fs.readFileSync(tsconfigPath, 'utf-8'));
const baseUrl = tsconfig.baseUrl;
assert(typeof baseUrl === 'string' || baseUrl === undefined);
const baseDir = baseUrl ? path.resolve(tsconfigDir, baseUrl) : tsconfigDir;
const paths = tsconfig['compilerOptions']['paths'];
assert(typeof paths === 'object');
const endWildcard = /\/\*$/;
return Object.fromEntries(
Object.entries(paths).map(([from, toArr]) => {
assert(typeof from === 'string', typeof from);
assert(Array.isArray(toArr) && toArr.length === 1);
let to = toArr[0];
assert(typeof to === 'string');
assert(endWildcard.test(from));
assert(endWildcard.test(to));
from = from.replace(endWildcard, '');
to = to.replace(endWildcard, '');
return [from, path.resolve(baseDir, to)];
})
);
}
module.exports = {
monorepoResourceNameMapper,
cacheConfig,
tsconfigResolveAliases,
};

255
build/perfCompare.py Normal file
View File

@@ -0,0 +1,255 @@
#! /usr/bin/env python3
# perfCompare.py
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT license.
#
# Benchmarks pyright's type-checking performance across two or more git
# commits/branches by building the production CLI bundle at each commit and
# timing repeated runs over a fixed corpus. See the module docstring for usage
# and the measurement methodology.
"""Compare pyright type-checking performance across two or more commits/branches.
Adapted from mypy's misc/perf_compare.py for pyright. Where mypy self-checks its
own (mypyc-compiled) source, pyright is a bundled JS CLI, so this script:
* For each target commit: checks it out *in place*, builds the production CLI
bundle (``packages/pyright/dist``), and stashes that dist dir under
``build/perfCompare/binaries/<sha>/`` so later runs can skip the rebuild.
* Restores your original branch/commit when the build phase is done.
* Runs each commit's bundle N times over a fixed corpus, in randomized
interleaved order, parsing pyright's own ``Completed in X.XXXsec`` line as the
per-run metric (in-process analysis time -- excludes node startup jitter).
* Reports per-commit mean/median plus robust paired deltas vs the baseline.
Usage:
python build/perfCompare.py --corpus <path-to-python-project> main HEAD
Requirements / caveats:
* Run from the pyright repo root with a CLEAN working tree (it checks out
commits in place, reusing the already-installed node_modules -- worktrees
would lack them).
* The corpus path is whatever you want type-checked; it stays fixed across all
commits so only the checker changes. Errors in the corpus are ignored.
* Builds are keyed by the resolved commit SHA, so passing a moving ref like
HEAD/branch name caches under its current SHA (fine within one run).
"""
import argparse
import os
import random
import re
import resource
import shutil
import statistics
import subprocess
import sys
import time
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_ROOT = os.path.dirname(SCRIPT_DIR)
PYRIGHT_PKG = os.path.join(REPO_ROOT, "packages", "pyright")
DIST_DIR = os.path.join(PYRIGHT_PKG, "dist")
# Cached production bundles, keyed by commit SHA, kept beside this script (gitignored).
BUILD_CACHE = os.path.join(SCRIPT_DIR, "perfCompare", "binaries")
COMPLETED_RE = re.compile(r"Completed in ([\d.]+)sec")
CHECK_RE = re.compile(r"Check:\s+([\d.]+)sec")
def heading(s: str) -> None:
print()
print(f"=== {s} ===")
print()
def git(*args: str, capture: bool = False) -> str:
res = subprocess.run(
["git", *args], cwd=REPO_ROOT, check=True, text=True,
stdout=subprocess.PIPE if capture else None,
)
return (res.stdout or "").strip()
def resolve_sha(commit: str) -> str:
return git("rev-parse", commit, capture=True)
def current_ref() -> str:
"""The symbolic branch name if on one, else the detached SHA -- for restore."""
branch = git("rev-parse", "--abbrev-ref", "HEAD", capture=True)
return branch if branch != "HEAD" else resolve_sha("HEAD")
def working_tree_clean() -> bool:
return git("status", "--porcelain", capture=True) == ""
def build_commit(commit: str, sha: str) -> str:
"""Check out `commit`, build the prod bundle, cache dist under the SHA. Returns cache dir."""
cache_dir = os.path.join(BUILD_CACHE, sha)
if os.path.isdir(cache_dir) and os.path.isfile(os.path.join(cache_dir, "pyright.js")):
print(f"build cache hit for {commit} ({sha[:10]}): {cache_dir}")
return cache_dir
heading(f"Building {commit} ({sha[:10]})")
git("checkout", "-q", sha)
subprocess.run(["npm", "run", "build"], cwd=PYRIGHT_PKG, check=True)
if os.path.isdir(cache_dir):
shutil.rmtree(cache_dir)
os.makedirs(BUILD_CACHE, exist_ok=True)
shutil.copytree(DIST_DIR, cache_dir, symlinks=True)
print(f"cached build -> {cache_dir}")
return cache_dir
def run_once(bundle_dir: str, corpus: str) -> tuple[float, float | None, float]:
"""Run pyright on corpus once; return (wall_completed_sec, check_sec | None, cpu_sec).
`wall` is pyright's own ``Completed in`` (in-process analysis wall time). `cpu` is the
user+sys CPU time of the node child, via the RUSAGE_CHILDREN delta around the run --
much less sensitive to scheduling/background noise. This is accurate because pyright runs
single-process by default (no ``--threads``), so there are no un-reaped worker processes
whose CPU would escape node's accounting.
"""
cmd = ["node", os.path.join(bundle_dir, "pyright.js"), "--stats", os.path.abspath(corpus)]
r0 = resource.getrusage(resource.RUSAGE_CHILDREN)
# Ignore exit code: corpora routinely produce type errors.
res = subprocess.run(cmd, cwd=REPO_ROOT, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
r1 = resource.getrusage(resource.RUSAGE_CHILDREN)
cpu = (r1.ru_utime - r0.ru_utime) + (r1.ru_stime - r0.ru_stime)
m = COMPLETED_RE.search(res.stdout)
if not m:
sys.stderr.write(res.stdout[-2000:] + "\n")
raise RuntimeError("Could not parse 'Completed in ...sec' from pyright output")
c = CHECK_RE.search(res.stdout)
return float(m.group(1)), (float(c.group(1)) if c else None), cpu
def winsorized_paired_stats(diffs: list[float], *, trim_frac: float = 0.1, conf: float = 0.95) -> dict[str, float]:
"""Robust paired-difference summary (trimmed mean + Tukey-McLaughlin SE). See mypy's perf_compare."""
n = len(diffs)
s = sorted(diffs)
g = int(n * trim_frac)
median = statistics.median(s)
if n < 2 or n - 2 * g < 2:
return {"est": statistics.mean(s), "median": median, "ci": 0.0, "kept": float(n)}
kept = s[g: n - g]
est = statistics.mean(kept)
wins = [kept[0]] * g + kept + [kept[-1]] * g
wvar = statistics.variance(wins)
se = (wvar ** 0.5) / ((1 - 2 * trim_frac) * (n ** 0.5))
z = statistics.NormalDist().inv_cdf(0.5 + conf / 2)
return {"est": est, "median": median, "ci": z * se, "kept": float(len(kept))}
def main() -> None:
t0 = time.time()
p = argparse.ArgumentParser(
formatter_class=argparse.RawDescriptionHelpFormatter, description=__doc__
)
src = p.add_mutually_exclusive_group(required=True)
src.add_argument("--corpus", help="path to a Python project *directory* to type-check (fixed across commits)")
src.add_argument("--corpus-file", help="path to a single Python *file* to type-check (e.g. one hot file). "
"Lower per-run cost and variance than a whole project.")
p.add_argument("--num-runs", type=int, default=10, help="measured runs per commit (default 10)")
p.add_argument("--warmup-runs", type=int, default=2, help="leading warmup runs to discard (default 2)")
p.add_argument("--metric", choices=["wall", "cpu"], default="wall",
help="quantity to compare: 'wall' (pyright's reported analysis time, default) or "
"'cpu' (user+sys CPU of the node child). 'cpu' is far less sensitive to background "
"interference/scheduling, tightening the distribution -- recommended for high run counts.")
p.add_argument("--workers1", action="store_true",
help="parity flag with mypy's perf_compare: assert single-process analysis. Pyright already "
"runs single-process by default (multi-process needs --threads, which this script never "
"passes), so this is the default; the flag is accepted to document intent.")
p.add_argument("--no-build", action="store_true", help="skip building; reuse cached dist bundles under build/perfCompare/binaries/")
p.add_argument("commit", nargs="+", help="git revisions to compare; the first is the baseline (e.g. main HEAD)")
args = p.parse_args()
if not os.path.isdir(os.path.join(REPO_ROOT, ".git")):
sys.exit("error: run from the pyright repo root")
corpus = args.corpus if args.corpus is not None else args.corpus_file
if args.corpus is not None and not os.path.isdir(args.corpus):
sys.exit(f"error: --corpus directory not found: {args.corpus}")
if args.corpus_file is not None and not os.path.isfile(args.corpus_file):
sys.exit(f"error: --corpus-file not found: {args.corpus_file}")
commits: list[str] = args.commit
shas = {c: resolve_sha(c) for c in commits}
bundles: dict[str, str] = {}
if args.no_build:
for c in commits:
cache_dir = os.path.join(BUILD_CACHE, shas[c])
if not os.path.isfile(os.path.join(cache_dir, "pyright.js")):
sys.exit(f"error: --no-build but no cached bundle for {c} ({shas[c][:10]})")
bundles[c] = cache_dir
else:
if not working_tree_clean():
sys.exit("error: working tree not clean -- commit/stash first (this script checks out commits in place)")
original = current_ref()
print(f"original ref: {original} (will restore after building)")
try:
for c in commits:
bundles[c] = build_commit(c, shas[c])
finally:
heading(f"Restoring {original}")
git("checkout", "-q", original)
num_runs = args.num_runs + args.warmup_runs
metric_label = "CPU time (user+sys)" if args.metric == "cpu" else "pyright-reported wall time"
heading(f"Measuring (corpus: {corpus}, {args.num_runs} runs + {args.warmup_runs} warmup, "
f"metric: {args.metric}, single-process)")
# `results` holds the chosen comparison metric; the other values are still printed per run.
results: dict[str, list[float]] = {c: [] for c in commits}
for n in range(num_runs):
warm = n < args.warmup_runs
print(f"{'Warmup' if warm else 'Run'} {n + 1 - (0 if warm else args.warmup_runs)}/"
f"{args.warmup_runs if warm else args.num_runs}...")
order = commits[:]
random.shuffle(order)
for c in order:
wall, check, cpu = run_once(bundles[c], corpus)
metric_val = cpu if args.metric == "cpu" else wall
if not warm:
results[c].append(metric_val)
check_str = f" check={check:.2f}s" if check is not None else ""
print(f" {c}: cpu={cpu:.3f}s wall={wall:.3f}s{check_str}")
baseline = commits[0]
heading(f"Results ({metric_label})")
first_mean = first_median = -1.0
for c in commits:
mean = statistics.mean(results[c])
median = statistics.median(results[c])
sd = statistics.pstdev(results[c]) if len(results[c]) > 1 else 0.0
if first_mean < 0:
first_mean, first_median = mean, median
dm = dmed = "0.0%"
else:
dm = f"{mean / first_mean - 1:+.1%}"
dmed = f"{median / first_median - 1:+.1%}"
print(f"{c:<22} mean {mean:.3f}s ({dm}) | stdev {sd:.3f}s | median {median:.3f}s ({dmed})")
base_runs = results[baseline]
base_center = statistics.median(base_runs)
heading(f"Paired deltas vs {baseline} (per-round diffs; median +/- 95% CI)")
for c in commits:
if c == baseline:
print(f"{c:<22} baseline")
continue
diffs = [a - b for a, b in zip(results[c], base_runs)]
st = winsorized_paired_stats(diffs)
pct = (st["median"] / base_center * 100) if base_center else 0.0
print(f"{c:<22} median {st['median'] * 1000:+7.1f}ms +/-{st['ci'] * 1000:4.1f} ({pct:+.2f}%)")
t = int(time.time() - t0)
print(f"\nTotal wall time: {t // 60}m {t % 60}s")
if __name__ == "__main__":
main()

9
build/skipBootstrap.js Normal file
View File

@@ -0,0 +1,9 @@
// This script exits with a "failure" if this SKIP_LERNA_BOOTSTRAP is set.
// This can be used to write npm script like:
// node ./build/skipBootstrap.js || lerna bootstrap
// Which means "skip lerna bootstrap if SKIP_LERNA_BOOTSTRAP is set".
// This prevents spurious bootstraps in nested lerna repos.
if (!process.env.SKIP_LERNA_BOOTSTRAP) {
process.exit(1);
}

View File

@@ -0,0 +1,16 @@
steps:
- task: npmAuthenticate@0
inputs:
workingFile: .npmrc
- task: npmAuthenticate@0
inputs:
workingFile: packages/pyright/.npmrc
- task: npmAuthenticate@0
inputs:
workingFile: packages/pyright-internal/.npmrc
- task: npmAuthenticate@0
inputs:
workingFile: packages/vscode-pyright/.npmrc

29
build/updateDeps.js Normal file
View File

@@ -0,0 +1,29 @@
/* eslint-disable @typescript-eslint/no-var-requires */
//@ts-check
const yargs = require('yargs');
const { updateAll } = require('./lib/updateDeps');
async function main() {
const argv = yargs.options({
transitive: { type: 'boolean' },
}).argv;
await updateAll(!!argv.transitive, [
// These packages impact compatibility with VS Code and other users;
// ensure they remained pinned exactly.
'@types/vscode',
'vsce',
'vscode-jsonrpc',
'vscode-languageclient',
'vscode-languageserver',
'vscode-languageserver-protocol',
'vscode-languageserver-textdocument',
'vscode-languageserver-types',
// Minor version changes have breaks; require a manual update.
'typescript',
]);
}
main();

251
build/updateTypeshed.py Normal file
View File

@@ -0,0 +1,251 @@
#!/usr/bin/env python3
"""
Script to update the typeshed-fallback folder with the latest files from
the typeshed repository (https://github.com/python/typeshed).
This script:
1. Clones/downloads the typeshed repository to a temporary directory
2. Copies the stdlib/ and stubs/ folders to typeshed-fallback
3. Copies the LICENSE and README.md files
4. Updates commit.txt with the current commit hash
"""
import argparse
import os
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
def get_script_dir() -> Path:
"""Get the directory where this script is located."""
return Path(__file__).parent.resolve()
def get_typeshed_fallback_dir() -> Path:
"""Get the path to the typeshed-fallback directory."""
script_dir = get_script_dir()
return script_dir.parent / "packages" / "pyright-internal" / "typeshed-fallback"
def run_git_command(args: list[str], cwd: Path) -> subprocess.CompletedProcess:
"""Run a git command and return the result."""
return subprocess.run(
["git"] + args,
cwd=cwd,
capture_output=True,
text=True,
check=True,
)
def clone_typeshed(target_dir: Path, commit: str | None = None) -> str:
"""
Clone the typeshed repository to the target directory.
Args:
target_dir: Directory to clone into
commit: Optional specific commit hash to checkout
Returns:
The commit hash that was checked out
"""
typeshed_url = "https://github.com/python/typeshed.git"
print(f"Cloning typeshed repository to {target_dir}...")
# Clone with depth 1 for faster download (unless we need a specific commit)
if commit:
# Full clone needed for specific commit
run_git_command(["clone", typeshed_url, str(target_dir)], cwd=target_dir.parent)
run_git_command(["checkout", commit], cwd=target_dir)
else:
# Shallow clone for latest
run_git_command(["clone", "--depth", "1", typeshed_url, str(target_dir)], cwd=target_dir.parent)
# Get the current commit hash
result = run_git_command(["rev-parse", "HEAD"], cwd=target_dir)
commit_hash = result.stdout.strip()
print(f"Checked out commit: {commit_hash}")
return commit_hash
def remove_directory_contents(dir_path: Path) -> None:
"""Remove all contents of a directory but keep the directory itself."""
if dir_path.exists():
shutil.rmtree(dir_path)
dir_path.mkdir(parents=True, exist_ok=True)
def should_copy_file(file_path: Path) -> bool:
"""Check if a file should be copied based on its extension or name."""
allowed_extensions = {".pyi", ".toml"}
allowed_names = {"VERSIONS"}
return file_path.suffix.lower() in allowed_extensions or file_path.name in allowed_names
def is_in_excluded_folder(file_path: Path, base_folder: Path) -> bool:
"""Check if the file is inside a folder that starts with '@'."""
rel_path = file_path.relative_to(base_folder)
for part in rel_path.parts:
if part.startswith("@"):
return True
return False
def copy_tree_filtered(src_folder: Path, dst_folder: Path) -> None:
"""
Copy a directory tree, only including .pyi and VERSIONS files.
Skips any folder starting with '@'.
Args:
src_folder: Source directory
dst_folder: Destination directory
"""
for src_path in src_folder.rglob("*"):
if src_path.is_file() and should_copy_file(src_path):
# Skip files in folders starting with '@'
if is_in_excluded_folder(src_path, src_folder):
continue
# Calculate relative path and destination
rel_path = src_path.relative_to(src_folder)
dst_path = dst_folder / rel_path
# Create parent directories if needed
dst_path.parent.mkdir(parents=True, exist_ok=True)
# Copy the file
shutil.copy2(src_path, dst_path)
def copy_typeshed_files(source_dir: Path, dest_dir: Path) -> None:
"""
Copy the relevant typeshed files to the destination directory.
Only .pyi and VERSIONS files are copied from folders.
Args:
source_dir: The cloned typeshed repository directory
dest_dir: The typeshed-fallback directory
"""
# Folders to copy
folders_to_copy = ["stdlib", "stubs"]
# Copy folders (only .py and .pyi files)
for folder in folders_to_copy:
src_folder = source_dir / folder
dst_folder = dest_dir / folder
if not src_folder.exists():
print(f"Warning: Source folder {src_folder} does not exist, skipping...")
continue
print(f"Copying {folder}/ (only .pyi and VERSIONS files)...")
# Remove existing folder contents
remove_directory_contents(dst_folder)
# Copy the folder with filtering
copy_tree_filtered(src_folder, dst_folder)
# Files to copy
files_to_copy = ["LICENSE", "README.md"]
# Copy files
for file in files_to_copy:
src_file = source_dir / file
dst_file = dest_dir / file
if not src_file.exists():
print(f"Warning: Source file {src_file} does not exist, skipping...")
continue
print(f"Copying {file}...")
shutil.copy2(src_file, dst_file)
def update_commit_file(dest_dir: Path, commit_hash: str) -> None:
"""Update the commit.txt file with the new commit hash."""
commit_file = dest_dir / "commit.txt"
print(f"Updating commit.txt with {commit_hash}...")
commit_file.write_text(commit_hash + "\n")
def main() -> int:
parser = argparse.ArgumentParser(
description="Update typeshed-fallback with the latest typeshed files"
)
parser.add_argument(
"--commit",
"-c",
type=str,
default=None,
help="Specific commit hash to checkout (default: latest main branch)",
)
parser.add_argument(
"--dry-run",
"-n",
action="store_true",
help="Show what would be done without making changes",
)
args = parser.parse_args()
typeshed_fallback_dir = get_typeshed_fallback_dir()
if not typeshed_fallback_dir.exists():
print(f"Error: typeshed-fallback directory not found at {typeshed_fallback_dir}")
return 1
print(f"Typeshed fallback directory: {typeshed_fallback_dir}")
if args.dry_run:
print("\n*** DRY RUN - No changes will be made ***\n")
print("Would perform the following actions:")
print(" 1. Clone typeshed repository to a temporary directory")
if args.commit:
print(f" 2. Checkout commit: {args.commit}")
else:
print(" 2. Use latest commit from main branch")
print(" 3. Copy stdlib/ folder (only .pyi and VERSIONS files)")
print(" 4. Copy stubs/ folder (only .pyi and VERSIONS files)")
print(" 5. Copy LICENSE file")
print(" 6. Copy README.md file")
print(" 7. Update commit.txt with new commit hash")
return 0
# Create a temporary directory for cloning
with tempfile.TemporaryDirectory() as temp_dir:
temp_path = Path(temp_dir)
typeshed_clone_dir = temp_path / "typeshed"
try:
# Clone typeshed
commit_hash = clone_typeshed(typeshed_clone_dir, args.commit)
# Copy files
copy_typeshed_files(typeshed_clone_dir, typeshed_fallback_dir)
# Update commit.txt
update_commit_file(typeshed_fallback_dir, commit_hash)
print("\nTypeshed update complete!")
print(f"Updated to commit: {commit_hash}")
except subprocess.CalledProcessError as e:
print(f"Error running git command: {e}")
print(f"stdout: {e.stdout}")
print(f"stderr: {e.stderr}")
return 1
except Exception as e:
print(f"Error: {e}")
return 1
return 0
if __name__ == "__main__":
sys.exit(main())