Skip to content

Commit dc1053c

Browse files
fix(cli): expand TEMP and env paths for clone/image deployment (#3453)
* fix(cli): expand TEMP and env paths for clone/image deployment Recursive environment expansion fixes clone-from-image when TEMP is stored as unexpanded %LOCALAPPDATA%\\Temp. Centralizes temp resolution, expands --dest and local image paths, and adds unit tests. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(cli): fall back to Path.GetTempPath when TEMP expansion is incomplete Address Copilot review: centralize UserTemp on GetUserTempDirectory and use Path.GetTempPath when env expansion still contains % tokens. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b98535f commit dc1053c

6 files changed

Lines changed: 192 additions & 6 deletions

File tree

dev/pyRevitLabs/pyRevitLabs.Common/CommonUtils.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,44 @@ public static bool VerifyPath(string path) {
4343
return false;
4444
}
4545

46+
/// <summary>
47+
/// Expands %VAR% tokens in a user-supplied path (e.g. --dest=%LOCALAPPDATA%\pyRevit).
48+
/// Repeats until stable so nested values like TEMP=%LOCALAPPDATA%\Temp resolve fully.
49+
/// </summary>
50+
public static string ExpandEnvironmentPath(string path) {
51+
if (string.IsNullOrWhiteSpace(path))
52+
return path;
53+
return ExpandEnvironmentPathRecursive(path.Trim());
54+
}
55+
56+
/// <summary>
57+
/// User temp directory with environment variables expanded (handles TEMP=%LOCALAPPDATA%\Temp).
58+
/// </summary>
59+
public static string GetUserTempDirectory() {
60+
var temp = Environment.GetEnvironmentVariable("TEMP");
61+
var candidate = !string.IsNullOrWhiteSpace(temp)
62+
? ExpandEnvironmentPathRecursive(temp.Trim())
63+
: ExpandEnvironmentPathRecursive("%TEMP%");
64+
if (IsConcreteExpandedPath(candidate))
65+
return candidate;
66+
return Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
67+
}
68+
69+
private static bool IsConcreteExpandedPath(string path) {
70+
return !string.IsNullOrWhiteSpace(path) && path.IndexOf('%') < 0;
71+
}
72+
73+
private static string ExpandEnvironmentPathRecursive(string path) {
74+
var expanded = path;
75+
for (int pass = 0; pass < 8; pass++) {
76+
var next = Environment.ExpandEnvironmentVariables(expanded).Trim();
77+
if (string.Equals(next, expanded, StringComparison.OrdinalIgnoreCase))
78+
break;
79+
expanded = next;
80+
}
81+
return expanded;
82+
}
83+
4684
public static bool VerifyPythonScript(string path) {
4785
return VerifyFile(path) && path.ToLower().EndsWith(".py");
4886
}

dev/pyRevitLabs/pyRevitLabs.Common/UserEnv.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ public static bool IsRunAsAdmin() {
110110

111111
public static string UserHome => Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
112112

113-
public static string UserTemp => Environment.ExpandEnvironmentVariables("%TEMP%");
113+
public static string UserTemp => CommonUtils.GetUserTempDirectory();
114114

115115
private static string[] _knownFolderGuids = new string[]
116116
{

dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitClones.cs

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,12 @@ public static void DeployFromRepo(string cloneName,
225225
string destPath = null,
226226
GitInstallerCredentials credentials = null)
227227
{
228+
if (destPath != null)
229+
destPath = CommonUtils.ExpandEnvironmentPath(destPath);
230+
228231
string repoSourcePath = repoUrl ?? PyRevitLabsConsts.OriginalRepoGitPath;
232+
if (!repoSourcePath.IsValidHttpUrl())
233+
repoSourcePath = CommonUtils.ExpandEnvironmentPath(repoSourcePath);
229234
string repoBranch = branchName != null ? branchName : PyRevitLabsConsts.TargetBranch;
230235
logger.Debug("Repo source determined as \"{0}:{1}\"", repoSourcePath, repoBranch);
231236

@@ -309,6 +314,11 @@ public static void DeployFromImage(string cloneName,
309314
bool installBinaries = true,
310315
BinArtifactInstallMode binInstallMode = BinArtifactInstallMode.Clone)
311316
{
317+
if (destPath != null)
318+
destPath = CommonUtils.ExpandEnvironmentPath(destPath);
319+
if (imagePath != null)
320+
imagePath = CommonUtils.ExpandEnvironmentPath(imagePath);
321+
312322
string repoBranch = branchName != null ? branchName : PyRevitLabsConsts.TargetBranch;
313323
string imageSource = imagePath != null ? imagePath : GithubAPI.GetBranchArchiveUrl(PyRevitLabsConsts.OriginalRepoId, repoBranch);
314324
string imageFilePath = null;
@@ -342,7 +352,7 @@ public static void DeployFromImage(string cloneName,
342352
{
343353
try
344354
{
345-
var pkgDest = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Path.GetFileName(imageSource));
355+
var pkgDest = Path.Combine(CommonUtils.GetUserTempDirectory(), Path.GetFileName(imageSource));
346356
logger.Info("Downloading package \"{0}\"", imageSource);
347357
logger.Debug("Downloading package \"{0}\" to \"{1}\"", imageSource, pkgDest);
348358
imageFilePath =
@@ -375,7 +385,7 @@ public static void DeployFromImage(string cloneName,
375385
);
376386
}
377387
var stagedImage = Path.Combine(
378-
Environment.GetEnvironmentVariable("TEMP"),
388+
CommonUtils.GetUserTempDirectory(),
379389
Path.GetFileNameWithoutExtension(imageFilePath)
380390
);
381391

@@ -481,7 +491,10 @@ public static void DeployFromImage(string cloneName,
481491
}
482492
catch (PyRevitException ex)
483493
{
484-
logger.Error("Can not find a valid clone inside extracted package. | {0}", ex.Message);
494+
var errMsg = ex.Message;
495+
if (errMsg != null && errMsg.IndexOf('%') >= 0)
496+
errMsg += " Path may contain unexpanded environment variables; ensure TEMP and --dest resolve to absolute paths.";
497+
logger.Error("Can not find a valid clone inside extracted package. | {0}", errMsg);
485498
}
486499
}
487500

dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitExtensions.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -451,7 +451,7 @@ private static List<PyRevitExtensionDefinition> LookupExtensionInDefinitionFile(
451451
try {
452452
filePath =
453453
CommonUtils.DownloadFile(fileOrUri,
454-
Path.Combine(Environment.GetEnvironmentVariable("TEMP"),
454+
Path.Combine(CommonUtils.GetUserTempDirectory(),
455455
PyRevitConsts.EnvConfigsExtensionDBFileName)
456456
);
457457
}

dev/pyRevitLabs/pyRevitLabs.PyRevit/PyRevitRunner.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ public PyRevitRunnerExecEnv(PyRevitAttachment attachment) {
3636
// generate unique id for this execution
3737
ExecutionId = Guid.NewGuid().ToString();
3838
// setup working dir
39-
WorkingDirectory = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), ExecutionId);
39+
WorkingDirectory = Path.Combine(CommonUtils.GetUserTempDirectory(), ExecutionId);
4040
CommonUtils.EnsurePath(WorkingDirectory);
4141
}
4242

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
using System;
2+
using System.IO;
3+
using System.IO.Compression;
4+
5+
using Microsoft.VisualStudio.TestTools.UnitTesting;
6+
7+
using pyRevitLabs.Common;
8+
using pyRevitLabs.PyRevit;
9+
10+
namespace pyRevitLabs.UnitTests {
11+
[TestClass]
12+
public class CommonUtilsPathTests {
13+
private string _tempRoot;
14+
private string _previousPyRevitPathOverride;
15+
private string _previousTemp;
16+
private string _previousLocalAppData;
17+
18+
[TestInitialize]
19+
public void Setup() {
20+
_tempRoot = Path.Combine(Path.GetTempPath(), "pyRevitPathTests_" + Guid.NewGuid().ToString("N"));
21+
Directory.CreateDirectory(_tempRoot);
22+
_previousPyRevitPathOverride = Environment.GetEnvironmentVariable(PyRevitLabsConsts.PyRevitPathOverrideEnvVar);
23+
_previousTemp = Environment.GetEnvironmentVariable("TEMP");
24+
_previousLocalAppData = Environment.GetEnvironmentVariable("LOCALAPPDATA");
25+
Environment.SetEnvironmentVariable(
26+
PyRevitLabsConsts.PyRevitPathOverrideEnvVar,
27+
Path.Combine(_tempRoot, "AppDataPyRevit"));
28+
Directory.CreateDirectory(PyRevitLabsConsts.PyRevitPath);
29+
}
30+
31+
[TestCleanup]
32+
public void Cleanup() {
33+
Environment.SetEnvironmentVariable(
34+
PyRevitLabsConsts.PyRevitPathOverrideEnvVar,
35+
_previousPyRevitPathOverride);
36+
Environment.SetEnvironmentVariable("TEMP", _previousTemp);
37+
Environment.SetEnvironmentVariable("LOCALAPPDATA", _previousLocalAppData);
38+
if (Directory.Exists(_tempRoot))
39+
Directory.Delete(_tempRoot, recursive: true);
40+
}
41+
42+
[TestMethod]
43+
public void ExpandEnvironmentPath_ExpandsNestedVariables() {
44+
Environment.SetEnvironmentVariable("LOCALAPPDATA", _tempRoot);
45+
var expanded = CommonUtils.ExpandEnvironmentPath("%LOCALAPPDATA%\\pyRevit");
46+
Assert.AreEqual(Path.Combine(_tempRoot, "pyRevit"), expanded);
47+
}
48+
49+
[TestMethod]
50+
public void GetUserTempDirectory_ExpandsPercentTemp() {
51+
Environment.SetEnvironmentVariable("LOCALAPPDATA", _tempRoot);
52+
Environment.SetEnvironmentVariable("TEMP", "%LOCALAPPDATA%\\Temp");
53+
var tempDir = CommonUtils.GetUserTempDirectory();
54+
Assert.IsFalse(tempDir.Contains("%"), "Temp path should not contain unexpanded variables.");
55+
StringAssert.StartsWith(tempDir, _tempRoot);
56+
Assert.IsTrue(tempDir.EndsWith("Temp", StringComparison.OrdinalIgnoreCase));
57+
}
58+
59+
[TestMethod]
60+
public void GetUserTempDirectory_FallsBackToPathGetTempPathWhenUnexpanded() {
61+
Environment.SetEnvironmentVariable("TEMP", "%NONEXISTENT_PYREVIT_VAR%\\Temp");
62+
var tempDir = CommonUtils.GetUserTempDirectory();
63+
Assert.IsFalse(tempDir.Contains("%"));
64+
var expected = Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
65+
Assert.AreEqual(expected, tempDir);
66+
}
67+
68+
[TestMethod]
69+
public void UserTemp_MatchesGetUserTempDirectory() {
70+
Environment.SetEnvironmentVariable("LOCALAPPDATA", _tempRoot);
71+
Environment.SetEnvironmentVariable("TEMP", "%LOCALAPPDATA%\\Temp");
72+
Assert.AreEqual(CommonUtils.GetUserTempDirectory(), UserEnv.UserTemp);
73+
}
74+
75+
[TestMethod]
76+
public void DeployFromImage_UsesExpandedTempForStaging() {
77+
var localAppData = Path.Combine(_tempRoot, "LocalAppData");
78+
Directory.CreateDirectory(localAppData);
79+
Environment.SetEnvironmentVariable("LOCALAPPDATA", localAppData);
80+
Environment.SetEnvironmentVariable("TEMP", "%LOCALAPPDATA%\\Temp");
81+
82+
var zipPath = Path.Combine(_tempRoot, "testclone.zip");
83+
var destPath = Path.Combine(_tempRoot, "cloneDest");
84+
var cloneName = "TempDeployTest";
85+
CreateMinimalCloneZip(zipPath);
86+
87+
try {
88+
PyRevitClones.DeployFromImage(
89+
cloneName: cloneName,
90+
deploymentName: "core",
91+
branchName: null,
92+
imagePath: zipPath,
93+
destPath: destPath,
94+
installBinaries: false);
95+
96+
Assert.IsTrue(Directory.Exists(Path.Combine(destPath, "bin")),
97+
"core deployment should copy bin when TEMP is expanded.");
98+
Assert.IsTrue(Directory.Exists(Path.Combine(destPath, "pyrevitlib", "pyrevit")),
99+
"core deployment should copy pyrevitlib when TEMP is expanded.");
100+
101+
var expandedTemp = CommonUtils.GetUserTempDirectory();
102+
Assert.IsFalse(expandedTemp.Contains("%"));
103+
Assert.IsTrue(Directory.Exists(expandedTemp),
104+
"Expanded temp directory should exist after staging.");
105+
}
106+
finally {
107+
try {
108+
var clone = PyRevitClones.GetRegisteredClone(cloneName);
109+
PyRevitClones.UnregisterClone(clone);
110+
}
111+
catch {
112+
// clone may not have registered if deploy failed
113+
}
114+
if (Directory.Exists(destPath))
115+
CommonUtils.DeleteDirectory(destPath);
116+
}
117+
}
118+
119+
private static void CreateMinimalCloneZip(string zipPath) {
120+
var root = Path.Combine(Path.GetDirectoryName(zipPath), "zipcontent");
121+
if (Directory.Exists(root))
122+
Directory.Delete(root, recursive: true);
123+
Directory.CreateDirectory(Path.Combine(root, "bin"));
124+
Directory.CreateDirectory(Path.Combine(root, "pyrevitlib", "pyrevit"));
125+
Directory.CreateDirectory(Path.Combine(root, "site-packages"));
126+
File.WriteAllText(
127+
Path.Combine(root, "pyRevitfile"),
128+
"[deployments]\r\ncore = ['bin', 'pyrevitlib', 'site-packages', 'pyRevitfile']\r\n");
129+
if (File.Exists(zipPath))
130+
File.Delete(zipPath);
131+
ZipFile.CreateFromDirectory(root, zipPath);
132+
Directory.Delete(root, recursive: true);
133+
}
134+
}
135+
}

0 commit comments

Comments
 (0)