Skip to content

Commit f2a974c

Browse files
committed
Pip Package Manager
1 parent 56392da commit f2a974c

File tree

1 file changed

+166
-138
lines changed

1 file changed

+166
-138
lines changed

CodeEdit/Features/LSP/Registry/PackageManagers/Sources/PipPackageManager.swift

Lines changed: 166 additions & 138 deletions
Original file line numberDiff line numberDiff line change
@@ -7,141 +7,169 @@
77

88
import Foundation
99

10-
// final class PipPackageManager: PackageManagerProtocol {
11-
// private let installationDirectory: URL
12-
//
13-
// let shellClient: ShellClient
14-
//
15-
// init(installationDirectory: URL) {
16-
// self.installationDirectory = installationDirectory
17-
// self.shellClient = .live()
18-
// }
19-
//
20-
// func initialize(in packagePath: URL) async throws {
21-
// guard await isInstalled() else {
22-
// throw PackageManagerError.packageManagerNotInstalled
23-
// }
24-
//
25-
// do {
26-
// try createDirectoryStructure(for: packagePath)
27-
// _ = try await executeInDirectory(
28-
// in: packagePath.path, ["python -m venv venv"]
29-
// )
30-
//
31-
// let requirementsPath = packagePath.appending(path: "requirements.txt")
32-
// if !FileManager.default.fileExists(atPath: requirementsPath.path) {
33-
// try "# Package requirements\n".write(to: requirementsPath, atomically: true, encoding: .utf8)
34-
// }
35-
// } catch {
36-
// throw PackageManagerError.initializationFailed(error.localizedDescription)
37-
// }
38-
// }
39-
//
40-
// func install(method: InstallationMethod) async throws {
41-
// guard case .standardPackage(let source) = method else {
42-
// throw PackageManagerError.invalidConfiguration
43-
// }
44-
//
45-
// let packagePath = installationDirectory.appending(path: source.entryName)
46-
// try await initialize(in: packagePath)
47-
//
48-
// do {
49-
// let pipCommand = getPipCommand(in: packagePath)
50-
// var installArgs = [pipCommand, "install"]
51-
//
52-
// if source.version.lowercased() != "latest" {
53-
// installArgs.append("\(source.pkgName)==\(source.version)")
54-
// } else {
55-
// installArgs.append(source.pkgName)
56-
// }
57-
//
58-
// let extras = source.options["extra"]
59-
// if let extras {
60-
// if let lastIndex = installArgs.indices.last {
61-
// installArgs[lastIndex] += "[\(extras)]"
62-
// }
63-
// }
64-
//
65-
// _ = try await executeInDirectory(in: packagePath.path, installArgs)
66-
// try await updateRequirements(packagePath: packagePath)
67-
// try await verifyInstallation(packagePath: packagePath, package: source.pkgName)
68-
// } catch {
69-
// throw error
70-
// }
71-
// }
72-
//
73-
// /// Get the binary path for a Python package
74-
// func getBinaryPath(for package: String) -> String {
75-
// let packagePath = installationDirectory.appending(path: package)
76-
// let customBinPath = packagePath.appending(path: "bin").appending(path: package).path
77-
// if FileManager.default.fileExists(atPath: customBinPath) {
78-
// return customBinPath
79-
// }
80-
// return packagePath.appending(path: "venv").appending(path: "bin").appending(path: package).path
81-
// }
82-
//
83-
// func isInstalled() async -> Bool {
84-
// let pipCommands = ["pip3 --version", "python3 -m pip --version"]
85-
// for command in pipCommands {
86-
// do {
87-
// let versionOutput = try await runCommand(command)
88-
// let versionPattern = #"pip \d+\.\d+"#
89-
// let output = versionOutput.reduce(into: "") {
90-
// $0 += $1.trimmingCharacters(in: .whitespacesAndNewlines)
91-
// }
92-
// if output.range(of: versionPattern, options: .regularExpression) != nil {
93-
// return true
94-
// }
95-
// } catch {
96-
// continue
97-
// }
98-
// }
99-
// return false
100-
// }
101-
//
102-
// // MARK: - Helper methods
103-
//
104-
// private func getPipCommand(in packagePath: URL) -> String {
105-
// let venvPip = "venv/bin/pip"
106-
// return FileManager.default.fileExists(atPath: packagePath.appending(path: venvPip).path)
107-
// ? venvPip
108-
// : "python -m pip"
109-
// }
110-
//
111-
// /// Update the requirements.txt file with the installed package and extras
112-
// private func updateRequirements(packagePath: URL) async throws {
113-
// let pipCommand = getPipCommand(in: packagePath)
114-
// let requirementsPath = packagePath.appending(path: "requirements.txt")
115-
//
116-
// let freezeOutput = try await executeInDirectory(
117-
// in: packagePath.path,
118-
// ["\(pipCommand)", "freeze"]
119-
// )
120-
//
121-
// let requirementsContent = freezeOutput.joined(separator: "\n") + "\n"
122-
// try requirementsContent.write(to: requirementsPath, atomically: true, encoding: .utf8)
123-
// }
124-
//
125-
// private func verifyInstallation(packagePath: URL, package: String) async throws {
126-
// let pipCommand = getPipCommand(in: packagePath)
127-
// let output = try await executeInDirectory(
128-
// in: packagePath.path, ["\(pipCommand)", "list", "--format=freeze"]
129-
// )
130-
//
131-
// // Normalize package names for comparison
132-
// let normalizedPackageHyphen = package.replacingOccurrences(of: "_", with: "-").lowercased()
133-
// let normalizedPackageUnderscore = package.replacingOccurrences(of: "-", with: "_").lowercased()
134-
//
135-
// // Check if the package name appears in requirements.txt
136-
// let installedPackages = output.map { line in
137-
// line.lowercased().split(separator: "=").first?.trimmingCharacters(in: .whitespacesAndNewlines)
138-
// }
139-
// let packageFound = installedPackages.contains { installedPackage in
140-
// installedPackage == normalizedPackageHyphen || installedPackage == normalizedPackageUnderscore
141-
// }
142-
//
143-
// guard packageFound else {
144-
// throw PackageManagerError.installationFailed("Package \(package) not found in pip list")
145-
// }
146-
// }
147-
// }
10+
final class PipPackageManager: PackageManagerProtocol {
11+
private let installationDirectory: URL
12+
13+
let shellClient: ShellClient
14+
15+
init(installationDirectory: URL) {
16+
self.installationDirectory = installationDirectory
17+
self.shellClient = .live()
18+
}
19+
20+
// MARK: - PackageManagerProtocol
21+
22+
func install(method installationMethod: InstallationMethod) throws -> [PackageManagerInstallStep] {
23+
guard case .standardPackage(let source) = installationMethod else {
24+
throw PackageManagerError.invalidConfiguration
25+
}
26+
27+
let packagePath = installationDirectory.appending(path: source.entryName)
28+
return [
29+
initialize(in: packagePath),
30+
runPipInstall(source, in: packagePath),
31+
updateRequirements(in: packagePath),
32+
verifyInstallation(source, in: packagePath)
33+
]
34+
}
35+
36+
func isInstalled(method installationMethod: InstallationMethod) -> PackageManagerInstallStep {
37+
PackageManagerInstallStep(name: "", confirmation: .none) { model in
38+
let pipCommands = ["pip3 --version", "python3 -m pip --version"]
39+
var didFindPip = false
40+
for command in pipCommands {
41+
do {
42+
let versionOutput = try await model.runCommand(command)
43+
let versionPattern = #"pip \d+\.\d+"#
44+
let output = versionOutput.reduce(into: "") {
45+
$0 += $1.trimmingCharacters(in: .whitespacesAndNewlines)
46+
}
47+
if output.range(of: versionPattern, options: .regularExpression) != nil {
48+
didFindPip = true
49+
break
50+
}
51+
} catch {
52+
continue
53+
}
54+
}
55+
guard didFindPip else {
56+
throw PackageManagerError.packageManagerNotInstalled
57+
}
58+
}
59+
60+
}
61+
62+
/// Get the binary path for a Python package
63+
func getBinaryPath(for package: String) -> String {
64+
let packagePath = installationDirectory.appending(path: package)
65+
let customBinPath = packagePath.appending(path: "bin").appending(path: package).path
66+
if FileManager.default.fileExists(atPath: customBinPath) {
67+
return customBinPath
68+
}
69+
return packagePath.appending(path: "venv").appending(path: "bin").appending(path: package).path
70+
}
71+
72+
// MARK: - Initialize
73+
74+
func initialize(in packagePath: URL) -> PackageManagerInstallStep {
75+
PackageManagerInstallStep(name: "Initialize Directory Structure", confirmation: .none) { model in
76+
try await model.createDirectoryStructure(for: packagePath)
77+
try await model.executeInDirectory(in: packagePath.path(percentEncoded: false), ["python -m venv venv"])
78+
79+
let requirementsPath = packagePath.appending(path: "requirements.txt")
80+
if !FileManager.default.fileExists(atPath: requirementsPath.path) {
81+
try "# Package requirements\n".write(to: requirementsPath, atomically: true, encoding: .utf8)
82+
}
83+
}
84+
}
85+
86+
// MARK: - Pip Install
87+
88+
func runPipInstall(_ source: PackageSource, in packagePath: URL) -> PackageManagerInstallStep {
89+
let pipCommand = getPipCommand(in: packagePath)
90+
return PackageManagerInstallStep(
91+
name: "Install Package Using pip",
92+
confirmation: .required(
93+
message: "This requires the pip package \(source.pkgName)."
94+
+ "\nAllow CodeEdit to install this package?"
95+
)
96+
) { model in
97+
var installArgs = [pipCommand, "install"]
98+
99+
if source.version.lowercased() != "latest" {
100+
installArgs.append("\(source.pkgName)==\(source.version)")
101+
} else {
102+
installArgs.append(source.pkgName)
103+
}
104+
105+
let extras = source.options["extra"]
106+
if let extras {
107+
if let lastIndex = installArgs.indices.last {
108+
installArgs[lastIndex] += "[\(extras)]"
109+
}
110+
}
111+
112+
try await model.executeInDirectory(in: packagePath.path, installArgs)
113+
}
114+
}
115+
116+
// MARK: - Update Requirements.txt
117+
118+
/// Update the requirements.txt file with the installed package and extras
119+
private func updateRequirements(in packagePath: URL) -> PackageManagerInstallStep {
120+
let pipCommand = getPipCommand(in: packagePath)
121+
return PackageManagerInstallStep(
122+
name: "Update requirements.txt",
123+
confirmation: .none
124+
) { model in
125+
let requirementsPath = packagePath.appending(path: "requirements.txt")
126+
127+
let freezeOutput = try await model.executeInDirectory(
128+
in: packagePath.path(percentEncoded: false),
129+
["\(pipCommand)", "freeze"]
130+
)
131+
132+
await model.status("Writing requirements to requirements.txt")
133+
let requirementsContent = freezeOutput.joined(separator: "\n") + "\n"
134+
try requirementsContent.write(to: requirementsPath, atomically: true, encoding: .utf8)
135+
}
136+
}
137+
138+
// MARK: - Verify Installation
139+
140+
private func verifyInstallation(_ source: PackageSource, in packagePath: URL) -> PackageManagerInstallStep {
141+
let pipCommand = getPipCommand(in: packagePath)
142+
return PackageManagerInstallStep(
143+
name: "Verify Installation",
144+
confirmation: .none
145+
) { model in
146+
let output = try await model.executeInDirectory(
147+
in: packagePath.path(percentEncoded: false),
148+
["\(pipCommand)", "list", "--format=freeze"]
149+
)
150+
151+
// Normalize package names for comparison
152+
let normalizedPackageHyphen = source.pkgName.replacingOccurrences(of: "_", with: "-").lowercased()
153+
let normalizedPackageUnderscore = source.pkgName.replacingOccurrences(of: "-", with: "_").lowercased()
154+
155+
// Check if the package name appears in requirements.txt
156+
let installedPackages = output.map { line in
157+
line.lowercased().split(separator: "=").first?.trimmingCharacters(in: .whitespacesAndNewlines)
158+
}
159+
let packageFound = installedPackages.contains { installedPackage in
160+
installedPackage == normalizedPackageHyphen || installedPackage == normalizedPackageUnderscore
161+
}
162+
163+
guard packageFound else {
164+
throw PackageManagerError.installationFailed("Package \(source.pkgName) not found in pip list")
165+
}
166+
}
167+
}
168+
169+
private func getPipCommand(in packagePath: URL) -> String {
170+
let venvPip = "venv/bin/pip"
171+
return FileManager.default.fileExists(atPath: packagePath.appending(path: venvPip).path)
172+
? venvPip
173+
: "python -m pip"
174+
}
175+
}

0 commit comments

Comments
 (0)