@@ -11,6 +11,7 @@ import (
11
11
12
12
"github.com/BurntSushi/toml"
13
13
"github.com/Masterminds/semver/v3"
14
+ "github.com/livekit/livekit-cli/v2/pkg/util"
14
15
)
15
16
16
17
// PackageInfo represents information about a package found in a project
@@ -30,6 +31,14 @@ type VersionCheckResult struct {
30
31
Error error
31
32
}
32
33
34
+ // SourceType indicates whether we're checking a lock file or package file
35
+ type SourceType int
36
+
37
+ const (
38
+ SourceTypePackage SourceType = iota // Package file (e.g., requirements.txt, package.json)
39
+ SourceTypeLock // Lock file (e.g., package-lock.json, poetry.lock)
40
+ )
41
+
33
42
// CheckSDKVersion performs a comprehensive check for livekit-agents packages
34
43
func CheckSDKVersion (dir string , projectType ProjectType , settingsMap map [string ]string ) error {
35
44
pythonMinSDKVersion := settingsMap ["python-min-sdk-version" ]
@@ -86,7 +95,7 @@ func detectProjectFiles(dir string, projectType ProjectType) []string {
86
95
"uv.lock" ,
87
96
}
88
97
for _ , filename := range pythonFiles {
89
- if path := filepath .Join (dir , filename ); fileExists (path ) {
98
+ if path := filepath .Join (dir , filename ); util . FileExists (path ) {
90
99
files = append (files , path )
91
100
}
92
101
}
@@ -99,7 +108,7 @@ func detectProjectFiles(dir string, projectType ProjectType) []string {
99
108
"bun.lockb" ,
100
109
}
101
110
for _ , filename := range nodeFiles {
102
- if path := filepath .Join (dir , filename ); fileExists (path ) {
111
+ if path := filepath .Join (dir , filename ); util . FileExists (path ) {
103
112
files = append (files , path )
104
113
}
105
114
}
@@ -156,6 +165,11 @@ func parsePythonPackageVersion(line string) (string, bool) {
156
165
return "latest" , true
157
166
}
158
167
168
+ // Convert Python operators to semver operators
169
+ if operator == "==" {
170
+ operator = "="
171
+ }
172
+
159
173
// clean up the version string if it contains multiple constraints
160
174
// handle comma-separated version constraints like ">=1.2.5,<2"
161
175
if strings .Contains (version , "," ) {
@@ -205,7 +219,7 @@ func checkRequirementsFile(filePath, minVersion string) VersionCheckResult {
205
219
206
220
version , found := parsePythonPackageVersion (line )
207
221
if found {
208
- satisfied , err := isVersionSatisfied (version , minVersion )
222
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypePackage )
209
223
return VersionCheckResult {
210
224
PackageInfo : PackageInfo {
211
225
Name : "livekit-agents" ,
@@ -243,7 +257,7 @@ func checkPyprojectToml(filePath, minVersion string) VersionCheckResult {
243
257
if line , ok := dep .(string ); ok {
244
258
version , found := parsePythonPackageVersion (line )
245
259
if found {
246
- satisfied , err := isVersionSatisfied (version , minVersion )
260
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypePackage )
247
261
return VersionCheckResult {
248
262
PackageInfo : PackageInfo {
249
263
Name : "livekit-agents" ,
@@ -281,7 +295,7 @@ func checkPipfile(filePath, minVersion string) VersionCheckResult {
281
295
version = "latest"
282
296
}
283
297
284
- satisfied , err := isVersionSatisfied (version , minVersion )
298
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypePackage )
285
299
return VersionCheckResult {
286
300
PackageInfo : PackageInfo {
287
301
Name : "livekit-agents" ,
@@ -326,7 +340,7 @@ func checkSetupPy(filePath, minVersion string) VersionCheckResult {
326
340
}
327
341
version , found := parsePythonPackageVersion (packageLine )
328
342
if found {
329
- satisfied , err := isVersionSatisfied (version , minVersion )
343
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypePackage )
330
344
return VersionCheckResult {
331
345
PackageInfo : PackageInfo {
332
346
Name : "livekit-agents" ,
@@ -360,7 +374,7 @@ func checkSetupCfg(filePath, minVersion string) VersionCheckResult {
360
374
if matches != nil {
361
375
version := strings .TrimSpace (matches [2 ])
362
376
363
- satisfied , err := isVersionSatisfied (version , minVersion )
377
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypePackage )
364
378
return VersionCheckResult {
365
379
PackageInfo : PackageInfo {
366
380
Name : "livekit-agents" ,
@@ -407,7 +421,7 @@ func checkPackageJSON(filePath, minVersion string) VersionCheckResult {
407
421
408
422
for _ , deps := range dependencyMaps {
409
423
if version , ok := deps ["@livekit/agents" ]; ok {
410
- satisfied , err := isVersionSatisfied (version , minVersion )
424
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypePackage )
411
425
return VersionCheckResult {
412
426
PackageInfo : PackageInfo {
413
427
Name : "@livekit/agents" ,
@@ -467,7 +481,7 @@ func checkPackageLockJSON(filePath, minVersion string) VersionCheckResult {
467
481
}
468
482
469
483
if dep , ok := lockJSON .Dependencies ["@livekit/agents" ]; ok {
470
- satisfied , err := isVersionSatisfied (dep .Version , minVersion )
484
+ satisfied , err := isVersionSatisfied (dep .Version , minVersion , SourceTypeLock )
471
485
return VersionCheckResult {
472
486
PackageInfo : PackageInfo {
473
487
Name : "@livekit/agents" ,
@@ -497,7 +511,7 @@ func checkYarnLock(filePath, minVersion string) VersionCheckResult {
497
511
matches := pattern .FindStringSubmatch (string (content ))
498
512
if matches != nil {
499
513
version := matches [1 ]
500
- satisfied , err := isVersionSatisfied (version , minVersion )
514
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypeLock )
501
515
return VersionCheckResult {
502
516
PackageInfo : PackageInfo {
503
517
Name : "@livekit/agents" ,
@@ -527,7 +541,7 @@ func checkPnpmLock(filePath, minVersion string) VersionCheckResult {
527
541
matches := pattern .FindStringSubmatch (string (content ))
528
542
if matches != nil {
529
543
version := strings .TrimSpace (matches [1 ])
530
- satisfied , err := isVersionSatisfied (version , minVersion )
544
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypeLock )
531
545
return VersionCheckResult {
532
546
PackageInfo : PackageInfo {
533
547
Name : "@livekit/agents" ,
@@ -557,7 +571,7 @@ func checkPoetryLock(filePath, minVersion string) VersionCheckResult {
557
571
matches := pattern .FindStringSubmatch (string (content ))
558
572
if matches != nil {
559
573
version := matches [1 ]
560
- satisfied , err := isVersionSatisfied (version , minVersion )
574
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypeLock )
561
575
return VersionCheckResult {
562
576
PackageInfo : PackageInfo {
563
577
Name : "livekit-agents" ,
@@ -587,7 +601,7 @@ func checkUvLock(filePath, minVersion string) VersionCheckResult {
587
601
matches := pattern .FindStringSubmatch (string (content ))
588
602
if matches != nil {
589
603
version := matches [1 ]
590
- satisfied , err := isVersionSatisfied (version , minVersion )
604
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypeLock )
591
605
return VersionCheckResult {
592
606
PackageInfo : PackageInfo {
593
607
Name : "livekit-agents" ,
@@ -617,7 +631,7 @@ func checkPipfileLock(filePath, minVersion string) VersionCheckResult {
617
631
matches := pattern .FindStringSubmatch (string (content ))
618
632
if matches != nil {
619
633
version := matches [1 ]
620
- satisfied , err := isVersionSatisfied (version , minVersion )
634
+ satisfied , err := isVersionSatisfied (version , minVersion , SourceTypeLock )
621
635
return VersionCheckResult {
622
636
PackageInfo : PackageInfo {
623
637
Name : "livekit-agents" ,
@@ -636,28 +650,89 @@ func checkPipfileLock(filePath, minVersion string) VersionCheckResult {
636
650
}
637
651
638
652
// isVersionSatisfied checks if a version satisfies the minimum requirement
639
- func isVersionSatisfied (version , minVersion string ) (bool , error ) {
653
+ func isVersionSatisfied (version , minVersion string , sourceType SourceType ) (bool , error ) {
640
654
// Handle special cases
641
655
if version == "latest" || version == "*" || version == "" {
642
656
return true , nil // Latest version always satisfies
643
657
}
644
658
645
- // Normalize version strings
646
- normalizedVersion := normalizeVersion (version )
647
- normalizedMin := normalizeVersion (minVersion )
659
+ switch sourceType {
660
+ case SourceTypeLock :
661
+ // For lock files, we have the exact version that was installed
662
+ // Check if this exact version is >= the minimum version
663
+ normalizedVersion := normalizeVersion (version )
664
+ v , err := semver .NewVersion (normalizedVersion )
665
+ if err != nil {
666
+ return false , fmt .Errorf ("invalid version format: %s" , version )
667
+ }
648
668
649
- // Parse versions
650
- v , err := semver .NewVersion (normalizedVersion )
651
- if err != nil {
652
- return false , fmt .Errorf ("invalid version format: %s" , version )
653
- }
669
+ min , err := semver .NewVersion (minVersion )
670
+ if err != nil {
671
+ return false , fmt .Errorf ("invalid minimum version format: %s" , minVersion )
672
+ }
654
673
655
- min , err := semver .NewVersion (normalizedMin )
656
- if err != nil {
657
- return false , fmt .Errorf ("invalid minimum version format: %s" , minVersion )
658
- }
674
+ // Check if the exact version is >= minimum version
675
+ return ! v .LessThan (min ), nil
676
+
677
+ case SourceTypePackage :
678
+ // For package files, we have a constraint that will be resolved at install time
679
+ // Check if this constraint would allow installing a version that satisfies the minimum requirement
680
+ packageConstraint , err := semver .NewConstraint (version )
681
+ if err != nil {
682
+ return false , fmt .Errorf ("invalid package constraint format: %s" , version )
683
+ }
684
+
685
+ min , err := semver .NewVersion (minVersion )
686
+ if err != nil {
687
+ return false , fmt .Errorf ("invalid minimum version format: %s" , minVersion )
688
+ }
659
689
660
- return ! v .LessThan (min ), nil
690
+ // Check if the package constraint would allow installing a version >= minimum
691
+ // We do this by checking if there exists a version >= minimum that satisfies the package constraint
692
+ if packageConstraint .Check (min ) {
693
+ // The minimum version satisfies the package constraint, so it would be installable
694
+ return true , nil
695
+ }
696
+
697
+ // Check if the package constraint allows any version >= minimum
698
+ // This handles cases like ">=1.5.0" where 1.0.0 doesn't satisfy it, but it would install 1.5.0+ which > 1.0.0
699
+ // We'll test a few strategic versions to see if any satisfy the package constraint and are >= minimum
700
+ testVersions := []string {
701
+ minVersion , // The minimum version itself
702
+ fmt .Sprintf ("%d.%d.%d" , min .Major ()+ 1 , 0 , 0 ),
703
+ fmt .Sprintf ("%d.%d.%d" , min .Major (), min .Minor ()+ 1 , 0 ),
704
+ fmt .Sprintf ("%d.%d.%d" , min .Major (), min .Minor (), min .Patch ()+ 1 ),
705
+ }
706
+
707
+ // Add more versions to cover edge cases
708
+ if min .Major () > 0 {
709
+ testVersions = append (testVersions , fmt .Sprintf ("%d.0.0" , min .Major ()))
710
+ }
711
+ if min .Minor () > 0 {
712
+ testVersions = append (testVersions , fmt .Sprintf ("%d.%d.0" , min .Major (), min .Minor ()))
713
+ }
714
+
715
+ // Add some specific versions that might be common in constraints
716
+ testVersions = append (testVersions ,
717
+ fmt .Sprintf ("%d.%d.0" , min .Major (), min .Minor ()+ 2 ),
718
+ fmt .Sprintf ("%d.%d.0" , min .Major (), min .Minor ()+ 5 ),
719
+ fmt .Sprintf ("%d.%d.0" , min .Major (), min .Minor ()+ 10 ),
720
+ )
721
+
722
+ for _ , testVersion := range testVersions {
723
+ if v , err := semver .NewVersion (testVersion ); err == nil {
724
+ // Check if this version is >= minimum and satisfies the package constraint
725
+ if ! v .LessThan (min ) && packageConstraint .Check (v ) {
726
+ return true , nil
727
+ }
728
+ }
729
+ }
730
+
731
+ return false , nil
732
+
733
+ default :
734
+ return false , fmt .Errorf ("unknown source type: %d" , sourceType )
735
+ }
661
736
}
662
737
663
738
// normalizeVersion normalizes version strings for semver parsing
@@ -666,10 +741,7 @@ func normalizeVersion(version string) string {
666
741
version = strings .TrimSpace (version )
667
742
version = strings .Trim (version , " \" '" )
668
743
669
- // Remove version specifiers that aren't part of the version itself
670
- version = regexp .MustCompile (`^[=~><!]+` ).ReplaceAllString (version , "" )
671
-
672
- // Handle npm version ranges
744
+ // Handle npm version ranges (^ and ~ are npm-specific, not semver constraints)
673
745
if strings .HasPrefix (version , "^" ) || strings .HasPrefix (version , "~" ) {
674
746
version = version [1 :]
675
747
}
@@ -732,9 +804,3 @@ func getTargetPackageName(projectType ProjectType) string {
732
804
return ""
733
805
}
734
806
}
735
-
736
- // fileExists checks if a file exists
737
- func fileExists (path string ) bool {
738
- _ , err := os .Stat (path )
739
- return err == nil
740
- }
0 commit comments