Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/actions/spelling/patterns.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ REQUIRE(?:[_A-Z]*)\(.*\)
8wekyb3d8bbwe
_tisf_sqliteReturnValue

# Compact installer log timestamp format
\bYYMMDDhhmmss\b

El proyecto .* diferentes

# Package family names and package full names
Expand Down
6 changes: 6 additions & 0 deletions doc/Settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,12 @@ In addition, there are special values that cover multiple channels. `default` i

Sets the default strategy for naming log files for installers that support it. `manifest` is the default and uses the manifest name. `timestamp` uses the date and time. `guid` uses a generated GUID. `shortguid` uses the first 8 characters of a generated GUID. Invalid values will revert to `manifest`.

The `manifest` strategy targets a complete log path of at most 150 UTF-16 code units, including the directory and `.log` extension. Existing names are retained when they fit. Otherwise, WinGet omits the package version and uses a compact `YYMMDDhhmmss` timestamp. If that still exceeds the preferred length, it uses an `hhmmss` timestamp and retains as much of the start of the package ID as fits, preserving whole Unicode characters. If no package-ID prefix fits, it falls back to an eight-character random name. The complete package identity and selected path remain available in WinGet's diagnostic log.

This leaves additional room for installers that append text to the supplied log name, but suffix compatibility is best effort, not guaranteed. The log directory is unchanged. Other configured naming strategies retain their formats when they fit within the Windows path limit. Any generated path that reaches `MAX_PATH` falls back to the eight-character random name. WinGet passes that fallback path to the installer even if it still reaches `MAX_PATH`; it does not omit arguments or display a warning because of the path length.

Explicit log paths, such as those supplied with `--log`, are not shortened or omitted by this policy. For configuration runs, `WinGetUserSettings` can select `logging.fileNameStrategy` for the execution user's settings; `WinGetPackage` does not expose a per-package log-path property.

```json
"logging": {
"fileNameStrategy": "manifest" | "timestamp" | "guid" | "shortguid"
Expand Down
7 changes: 7 additions & 0 deletions doc/release-notes/1.31/2036-shorter-installer-log-paths.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
## Bug Fixes

### Shorter automatic installer log paths

WinGet now shortens default installer log names as needed to target a complete path of 150 characters, leaving more room for installers that append their own log names. Names retain the package version and timestamp when they fit; otherwise, WinGet omits the version and uses a compact timestamp, then a package-ID prefix with a time-only timestamp, before falling back to a short random name.

This is a best-effort mitigation, not a guarantee for installer-added suffixes. The log directory and explicit log paths are unchanged. WinGet still passes the shortest fallback path to the installer if it exceeds the Windows path limit, without removing installer arguments.
2 changes: 1 addition & 1 deletion src/AppInstallerCLICore/Workflows/DownloadFlow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -751,7 +751,7 @@ namespace AppInstaller::CLI::Workflow
}

const auto& manifest = context.Get<Execution::Data::Manifest>();
context.Add<Execution::Data::DownloadDirectory>(downloadsDirectory / GetPathPart(manifest, '_', true));
context.Add<Execution::Data::DownloadDirectory>(downloadsDirectory / GetPathPart(manifest, '_', PathPartVersionStrategy::DropUnknownVersion));
}
}

Expand Down
149 changes: 101 additions & 48 deletions src/AppInstallerCLICore/Workflows/ShellExecuteInstallerHandler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ namespace AppInstaller::CLI::Workflow

const auto& installer = context.Get<Execution::Data::Installer>();
const auto& installerSwitches = installer->Switches;
std::string installerArgs = {};
std::string installerArgs;

// Construct install experience arg.
// SilentWithProgress is default, so look for it first.
Expand Down Expand Up @@ -159,67 +159,121 @@ namespace AppInstaller::CLI::Workflow
return installerArgs;
}

// Applies values to the template.
void PopulateInstallerArgsTemplate(Execution::Context& context, std::string& installerArgs)
std::filesystem::path GetAutomaticInstallerLogPath(Execution::Context& context)
{
// Populate <LogPath> with value from command line or temp path.
std::string logPath;
if (context.Args.Contains(Execution::Args::Type::Log))
// Chosen to allow a reported 108 character suffix from installer.
constexpr size_t preferredPathLength = 150;
auto directory = Runtime::GetPathTo(Runtime::PathName::DefaultLogLocation);
auto makePath = [&](const std::filesystem::path& stem)
{
logPath = context.Args.GetArg(Execution::Args::Type::Log);
}
else
auto result = directory / stem;
result += Logging::FileLogger::DefaultExt();
return result;
};
auto shortGuidPath = [&]()
{
return makePath(Utility::CreateNewGuidNameWString().substr(0, 8));
};
const auto time = std::chrono::system_clock::now();
const auto timestamp = Utility::TimePointToString(time, Utility::TimeFacet::ShortYearSecondPrecision | Utility::TimeFacet::Filename);
std::filesystem::path result;

switch (Settings::User().Get<Settings::Setting::LoggingFileNameStrategy>())
{
case Logging::LogNameStrategy::Manifest:
{
const auto& manifest = context.Get<Execution::Data::Manifest>();
const Logging::LogNameStrategy logNameStrategy = Settings::User().Get<Settings::Setting::LoggingFileNameStrategy>();
auto path = Runtime::GetPathTo(Runtime::PathName::DefaultLogLocation);
result = makePath(GetPathPart(manifest).native() + L'-' + Utility::ConvertToUTF16(timestamp));

switch (logNameStrategy)
if (result.native().size() > preferredPathLength)
{
case Logging::LogNameStrategy::Manifest:
// Use manifest ID and version for log file name
// Results in <DefaultLogLocation>\<ManifestId>.<ManifestVersion>-<Timestamp>.log
path /= GetPathPart(manifest);
path += '-';
path += Utility::GetCurrentTimeForFilename(true);
break;
case Logging::LogNameStrategy::Timestamp:
// Use only timestamp for log file name
// Results in <DefaultLogLocation>\<Timestamp>.log
path /= Utility::GetCurrentTimeForFilename(true);
break;
case Logging::LogNameStrategy::Guid:
// Use a GUID for log file name
// Results in <DefaultLogLocation>\<GUID>.log
path /= Utility::CreateNewGuidNameWString();
break;
case Logging::LogNameStrategy::ShortGuid:
// Use the first 8 characters of a GUID for log file name
// Results in <DefaultLogLocation>\<First 8 characters of GUID>.log
path /= Utility::CreateNewGuidNameWString().substr(0, 8);
break;
default:
// This should never happen due to validation when reading settings, but handle it just in case.
AICLI_LOG(CLI, Error, << "Unknown log naming strategy.");
THROW_HR(E_UNEXPECTED);
break;
auto compactTimestamp = timestamp;
compactTimestamp.erase(std::remove(compactTimestamp.begin(), compactTimestamp.end(), '-'), compactTimestamp.end());
const auto suffix = L"-" + Utility::ConvertToUTF16(compactTimestamp);
const auto id = GetPathPart(manifest, '.', PathPartVersionStrategy::NeverIncludeVersion);
result = makePath(id.native() + suffix);

if (result.native().size() > preferredPathLength)
{
const auto timeSuffix = L"-" + Utility::ConvertToUTF16(compactTimestamp.substr(6));
const auto fixedLength = makePath(timeSuffix).native().size();
if (fixedLength < preferredPathLength && !id.empty())
{
const auto idString = id.u8string();
const auto idBudget = preferredPathLength - fixedLength;
auto prefixLength = idBudget;
auto prefix = Utility::ConvertToUTF16(Utility::UTF8Substring(idString, 0, prefixLength));
// Substring counts graphemes, but the path budget counts UTF-16 code units.
while (prefix.size() > idBudget)
{
prefix = Utility::ConvertToUTF16(Utility::UTF8Substring(idString, 0, --prefixLength));
}
if (!prefix.empty())
{
result = makePath(prefix + timeSuffix);
}
}
}

if (result.native().size() > preferredPathLength)
{
result = shortGuidPath();
}
}
AICLI_LOG(CLI, Info, << "Installer log for " << manifest.Id << " [" << manifest.Version << "]: " << result.u8string());
break;
}
case Logging::LogNameStrategy::Timestamp:
result = makePath(timestamp);
break;
case Logging::LogNameStrategy::Guid:
result = makePath(Utility::CreateNewGuidNameWString());
break;
case Logging::LogNameStrategy::ShortGuid:
result = shortGuidPath();
break;
default:
AICLI_LOG(CLI, Error, << "Unknown log naming strategy.");
THROW_HR(E_UNEXPECTED);
}

// Add the extension to the log file regardless of naming strategy
path += Logging::FileLogger::DefaultExt();

logPath = path.u8string();
if (result.native().size() >= MAX_PATH)
{
result = shortGuidPath();
}
if (result.native().size() > preferredPathLength)
{
AICLI_LOG(CLI, Warning, << "Installer log path exceeds the preferred length of " << preferredPathLength << ": " << result.u8string());
}

if (Utility::FindAndReplace(installerArgs, std::string(ARG_TOKEN_LOGPATH), logPath))
return result;
}

// Applies values to the template.
void PopulateInstallerArgsTemplate(Execution::Context& context, std::string& installerArgs)
{
if (installerArgs.find(ARG_TOKEN_LOGPATH) != std::string::npos)
{
context.Add<Execution::Data::LogPath>(Utility::ConvertToUTF16(logPath));
std::filesystem::path logPath;
if (context.Args.Contains(Execution::Args::Type::Log))
{
logPath = Utility::ConvertToUTF16(context.Args.GetArg(Execution::Args::Type::Log));
}
else
{
logPath = GetAutomaticInstallerLogPath(context);
}

auto logPathString = logPath.u8string();
Utility::FindAndReplace(installerArgs, ARG_TOKEN_LOGPATH, logPathString);
context.Add<Execution::Data::LogPath>(std::move(logPath));
}

// Populate <InstallPath> with value from command line.
if (context.Args.Contains(Execution::Args::Type::InstallLocation))
{
Utility::FindAndReplace(installerArgs, std::string(ARG_TOKEN_INSTALLPATH), context.Args.GetArg(Execution::Args::Type::InstallLocation));
auto locationArg = context.Args.GetArg(Execution::Args::Type::InstallLocation);
Utility::FindAndReplace(installerArgs, ARG_TOKEN_INSTALLPATH, locationArg);
}

// Todo: language token support will be implemented later
Expand Down Expand Up @@ -334,8 +388,7 @@ namespace AppInstaller::CLI::Workflow
return;
}

std::string installerArgs = GetInstallerArgsTemplate(context);

auto installerArgs = GetInstallerArgsTemplate(context);
PopulateInstallerArgsTemplate(context, installerArgs);

AICLI_LOG(CLI, Info, << "Installer args: " << installerArgs);
Expand Down
7 changes: 3 additions & 4 deletions src/AppInstallerCLIE2ETests/Pinning.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ public void Setup()
// All tests use TestExeInstaller; try to clean it up for failure cases,
// then install the base version for pinning
TestCommon.RunAICLICommand("uninstall", "AppInstallerTest.TestExeInstaller");
TestCommon.RunAICLICommand("install", "AppInstallerTest.TestExeInstaller -v 1.0.0.0");
TestCommon.RunAICLICommand("install", "AppInstallerTest.TestExeInstaller -v 1.0.0.0 --custom \"/Version 1.0.0.0\"");
TestCommon.RunAICLICommand("pin remove", "AppInstallerTest.TestExeInstaller");
}

Expand Down Expand Up @@ -71,8 +71,7 @@ public void UpgradeWithPinningPin()
RunCommandResult result;
string installDir = Path.GetTempPath();

// The base version of this app does not log /Version, but it still includes the version number in the log file name.
Assert.That(TestCommon.VerifyTestExeInstalled(installDir, "1.0.0.0"), Is.True, "Base version installed");
Assert.That(TestCommon.VerifyTestExeInstalled(installDir, "/Version 1.0.0.0"), Is.True, "Base version installed");

result = TestCommon.RunAICLICommand("pin add", "AppInstallerTest.TestExeInstaller");
Assert.That(result.ExitCode, Is.EqualTo(Constants.ErrorCode.S_OK));
Expand All @@ -89,7 +88,7 @@ public void UpgradeWithPinningPin()
result = TestCommon.RunAICLICommand("upgrade", "--all");
Assert.That(result.ExitCode, Is.EqualTo(Constants.ErrorCode.S_OK), "Upgrade succeeds with nothing to upgrade");

Assert.That(TestCommon.VerifyTestExeInstalled(installDir, "1.0.0.0"), Is.True, "No newer version installed");
Assert.That(TestCommon.VerifyTestExeInstalled(installDir, "/Version 1.0.0.0"), Is.True, "No newer version installed");

result = TestCommon.RunAICLICommand("upgrade", "--include-pinned");
Assert.That(result.ExitCode, Is.EqualTo(Constants.ErrorCode.S_OK));
Expand Down
Loading
Loading