初始化上传
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
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:
323
build/azuredevops/azure-pipelines-release.yml
Normal file
323
build/azuredevops/azure-pipelines-release.yml
Normal 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
|
||||
62
build/azuredevops/azure-pipelines.yml
Normal file
62
build/azuredevops/azure-pipelines.yml
Normal 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
|
||||
206
build/generateUnicodeTables.py
Normal file
206
build/generateUnicodeTables.py
Normal 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
83
build/lib/updateDeps.js
Normal 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
125
build/lib/webpack.js
Normal 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
255
build/perfCompare.py
Normal 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
9
build/skipBootstrap.js
Normal 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);
|
||||
}
|
||||
16
build/templates/npmAuthenticate.yml
Normal file
16
build/templates/npmAuthenticate.yml
Normal 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
29
build/updateDeps.js
Normal 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
251
build/updateTypeshed.py
Normal 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())
|
||||
Reference in New Issue
Block a user