1 // Copyright (c) 2011 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 // The file contains the implementation of the mini_installer re-versioner.
6 // The main function (GenerateNextVersion) does the following in a temp dir:
7 // - Extracts and unpacks setup.exe and the Chrome-bin folder from
9 // - Inspects setup.exe to determine the current version.
10 // - Runs through all .dll and .exe files:
11 // - Replacing all occurrences of the Unicode version string in the files'
12 // resources with the updated string.
13 // - For all resources in which the string substitution is made, the binary
14 // form of the version is also replaced.
15 // - Re-packs setup.exe and Chrome-bin.
16 // - Inserts them into the target mini_installer.exe.
18 // This code assumes that the host program 1) initializes the process-wide
19 // CommandLine instance, and 2) resides in the output directory of a build
20 // tree. When #2 is not the case, the --7za_path command-line switch may be
21 // used to provide the (relative or absolute) path to the directory containing
24 #include "chrome/installer/test/alternate_version_generator.h"
34 #include "base/basictypes.h"
35 #include "base/command_line.h"
36 #include "base/files/file.h"
37 #include "base/files/file_enumerator.h"
38 #include "base/files/file_path.h"
39 #include "base/files/file_util.h"
40 #include "base/logging.h"
41 #include "base/path_service.h"
42 #include "base/process/launch.h"
43 #include "base/process/process_handle.h"
44 #include "base/strings/string_util.h"
45 #include "base/strings/utf_string_conversions.h"
46 #include "base/version.h"
47 #include "base/win/pe_image.h"
48 #include "base/win/scoped_handle.h"
49 #include "chrome/installer/test/pe_image_resources.h"
50 #include "chrome/installer/test/resource_loader.h"
51 #include "chrome/installer/test/resource_updater.h"
52 #include "chrome/installer/util/lzma_util.h"
56 const wchar_t k7zaExe
[] = L
"7za.exe";
57 const wchar_t k7zaPathRelative
[] = L
"..\\..\\third_party\\lzma_sdk\\Executable";
58 const wchar_t kB7
[] = L
"B7";
59 const wchar_t kBl
[] = L
"BL";
60 const wchar_t kChrome7z
[] = L
"chrome.7z";
61 const wchar_t kChromeBin
[] = L
"Chrome-bin";
62 const wchar_t kChromePacked7z
[] = L
"chrome.packed.7z";
63 const wchar_t kExe
[] = L
"exe";
64 const wchar_t kExpandExe
[] = L
"expand.exe";
65 const wchar_t kExtDll
[] = L
".dll";
66 const wchar_t kExtExe
[] = L
".exe";
67 const wchar_t kMakeCab
[] = L
"makecab.exe";
68 const wchar_t kSetupEx_
[] = L
"setup.ex_";
69 const wchar_t kSetupExe
[] = L
"setup.exe";
70 const char kSwitch7zaPath
[] = "7za_path";
71 const wchar_t kTempDirPrefix
[] = L
"mini_installer_test_temp";
73 // A helper class for creating and cleaning a temporary directory. A temporary
74 // directory is created in Initialize and destroyed (along with all of its
75 // contents) when the guard instance is destroyed.
76 class ScopedTempDirectory
{
78 ScopedTempDirectory() { }
79 ~ScopedTempDirectory() {
80 if (!directory_
.empty() && !base::DeleteFile(directory_
, true)) {
81 LOG(DFATAL
) << "Failed deleting temporary directory \""
82 << directory_
.value() << "\"";
85 // Creates a temporary directory.
87 DCHECK(directory_
.empty());
88 if (!base::CreateNewTempDirectory(&kTempDirPrefix
[0], &directory_
)) {
89 LOG(DFATAL
) << "Failed creating temporary directory.";
94 const base::FilePath
& directory() const {
95 DCHECK(!directory_
.empty());
100 base::FilePath directory_
;
101 DISALLOW_COPY_AND_ASSIGN(ScopedTempDirectory
);
102 }; // class ScopedTempDirectory
104 // A helper class for manipulating a Chrome product version.
105 class ChromeVersion
{
107 static ChromeVersion
FromHighLow(DWORD high
, DWORD low
) {
108 return ChromeVersion(static_cast<ULONGLONG
>(high
) << 32 |
109 static_cast<ULONGLONG
>(low
));
111 static ChromeVersion
FromString(const std::string
& version_string
) {
112 Version
version(version_string
);
113 DCHECK(version
.IsValid());
114 const std::vector
<uint16
>& c(version
.components());
115 return ChromeVersion(static_cast<ULONGLONG
>(c
[0]) << 48 |
116 static_cast<ULONGLONG
>(c
[1]) << 32 |
117 static_cast<ULONGLONG
>(c
[2]) << 16 |
118 static_cast<ULONGLONG
>(c
[3]));
122 explicit ChromeVersion(ULONGLONG value
) : version_(value
) { }
123 WORD
major() const { return static_cast<WORD
>(version_
>> 48); }
124 WORD
minor() const { return static_cast<WORD
>(version_
>> 32); }
125 WORD
build() const { return static_cast<WORD
>(version_
>> 16); }
126 WORD
patch() const { return static_cast<WORD
>(version_
); }
127 DWORD
high() const { return static_cast<DWORD
>(version_
>> 32); }
128 DWORD
low() const { return static_cast<DWORD
>(version_
); }
129 ULONGLONG
value() const { return version_
; }
130 void set_value(ULONGLONG value
) { version_
= value
; }
131 std::wstring
ToString() const;
134 }; // class ChromeVersion
136 std::wstring
ChromeVersion::ToString() const {
139 swprintf_s(&buffer
[0], arraysize(buffer
), L
"%hu.%hu.%hu.%hu",
140 major(), minor(), build(), patch());
141 DCHECK_NE(-1, string_len
);
142 DCHECK_GT(static_cast<int>(arraysize(buffer
)), string_len
);
143 return std::wstring(&buffer
[0], string_len
);
147 // A read/write mapping of a file.
148 // Note: base::MemoryMappedFile is not used because it doesn't support
149 // read/write mappings. Adding such support across all platforms for this
150 // Windows-only test code seems like overkill.
153 MappedFile() : size_(), mapping_(), view_() { }
155 bool Initialize(base::File file
);
156 void* data() const { return view_
; }
157 size_t size() const { return size_
; }
164 DISALLOW_COPY_AND_ASSIGN(MappedFile
);
165 }; // class MappedFile
167 MappedFile::~MappedFile() {
169 if (UnmapViewOfFile(view_
) == 0) {
170 PLOG(DFATAL
) << "MappedFile failed to unmap view.";
173 if (mapping_
!= NULL
) {
174 if (CloseHandle(mapping_
) == 0) {
175 PLOG(DFATAL
) << "Could not close file mapping handle.";
180 bool MappedFile::Initialize(base::File file
) {
181 DCHECK(mapping_
== NULL
);
183 base::File::Info file_info
;
185 if (file
.GetInfo(&file_info
)) {
186 if (file_info
.size
<=
187 static_cast<int64
>(std::numeric_limits
<DWORD
>::max())) {
188 mapping_
= CreateFileMapping(file
.GetPlatformFile(), NULL
, PAGE_READWRITE
,
189 0, static_cast<DWORD
>(file_info
.size
), NULL
);
190 if (mapping_
!= NULL
) {
191 view_
= MapViewOfFile(mapping_
, FILE_MAP_WRITE
, 0, 0,
192 static_cast<size_t>(file_info
.size
));
196 PLOG(DFATAL
) << "MapViewOfFile failed";
199 PLOG(DFATAL
) << "CreateFileMapping failed";
202 LOG(DFATAL
) << "Files larger than " << std::numeric_limits
<DWORD
>::max()
203 << " are not supported.";
206 PLOG(DFATAL
) << "file.GetInfo failed";
212 // Calls CreateProcess with good default parameters and waits for the process
213 // to terminate returning the process exit code.
214 bool RunProcessAndWait(const wchar_t* exe_path
, const std::wstring
& cmdline
,
217 base::LaunchOptions options
;
219 options
.start_hidden
= true;
220 base::Process process
= base::LaunchProcess(cmdline
, options
);
221 if (process
.IsValid()) {
223 if (!GetExitCodeProcess(process
.Handle(),
224 reinterpret_cast<DWORD
*>(exit_code
))) {
225 PLOG(DFATAL
) << "Failed getting the exit code for \""
229 DCHECK_NE(*exit_code
, STILL_ACTIVE
);
239 // Retrieves the version number of |pe_file| from its version
240 // resource, placing the value in |version|. Returns true on success.
241 bool GetFileVersion(const base::FilePath
& pe_file
, ChromeVersion
* version
) {
244 upgrade_test::ResourceLoader pe_file_loader
;
245 std::pair
<const uint8
*, DWORD
> version_info_data
;
247 if (pe_file_loader
.Initialize(pe_file
) &&
248 pe_file_loader
.Load(VS_VERSION_INFO
, reinterpret_cast<WORD
>(RT_VERSION
),
249 &version_info_data
)) {
250 const VS_FIXEDFILEINFO
* fixed_file_info
;
252 if (VerQueryValue(version_info_data
.first
, L
"\\",
253 reinterpret_cast<void**>(
254 const_cast<VS_FIXEDFILEINFO
**>(&fixed_file_info
)),
255 &ver_info_len
) != 0) {
256 DCHECK_EQ(sizeof(VS_FIXEDFILEINFO
), static_cast<size_t>(ver_info_len
));
257 *version
= ChromeVersion::FromHighLow(fixed_file_info
->dwFileVersionMS
,
258 fixed_file_info
->dwFileVersionLS
);
261 LOG(DFATAL
) << "VerQueryValue failed to retrieve VS_FIXEDFILEINFO";
268 // Retrieves the version number of setup.exe in |work_dir| from its version
269 // resource, placing the value in |version|. Returns true on success.
270 bool GetSetupExeVersion(const base::FilePath
& work_dir
,
271 ChromeVersion
* version
) {
272 return GetFileVersion(work_dir
.Append(&kSetupExe
[0]), version
);
276 // Replace all occurrences in the sequence [|dest_first|, |dest_last) that
277 // equals [|src_first|, |src_last) with the sequence at |replacement_first| of
278 // the same length. Returns true on success. If non-NULL, |replacements_made|
279 // is set to true/false accordingly.
280 bool ReplaceAll(uint8
* dest_first
, uint8
* dest_last
,
281 const uint8
* src_first
, const uint8
* src_last
,
282 const uint8
* replacement_first
, bool* replacements_made
) {
284 bool changed
= false;
286 dest_first
= std::search(dest_first
, dest_last
, src_first
, src_last
);
287 if (dest_first
== dest_last
) {
291 if (memcpy_s(dest_first
, dest_last
- dest_first
,
292 replacement_first
, src_last
- src_first
) != 0) {
296 dest_first
+= (src_last
- src_first
);
299 if (replacements_made
!= NULL
) {
300 *replacements_made
= changed
;
306 // A context structure in support of our EnumResource_Fn callback.
307 struct VisitResourceContext
{
308 ChromeVersion current_version
;
309 std::wstring current_version_str
;
310 ChromeVersion new_version
;
311 std::wstring new_version_str
;
312 }; // struct VisitResourceContext
314 // Replaces the old version with the new in a resource. A first pass is made to
315 // replace the string form (e.g., "9.0.584.0"). If any replacements are made, a
316 // second pass is made to replace the binary form (e.g., 0x0000024800000009).
317 void VisitResource(const upgrade_test::EntryPath
& path
,
318 uint8
* data
, DWORD size
, DWORD code_page
,
320 VisitResourceContext
& ctx
= *reinterpret_cast<VisitResourceContext
*>(context
);
322 // Replace all occurrences of current_version_str with new_version_str
323 bool changing_version
= false;
327 reinterpret_cast<const uint8
*>(ctx
.current_version_str
.c_str()),
328 reinterpret_cast<const uint8
*>(ctx
.current_version_str
.c_str() +
329 ctx
.current_version_str
.size() + 1),
330 reinterpret_cast<const uint8
*>(ctx
.new_version_str
.c_str()),
331 &changing_version
) &&
333 // Replace all occurrences of current_version with new_version
338 VersionPair cur_ver
= {
339 ctx
.current_version
.high(), ctx
.current_version
.low()
341 VersionPair new_ver
= {
342 ctx
.new_version
.high(), ctx
.new_version
.low()
344 ReplaceAll(data
, data
+ size
, reinterpret_cast<const uint8
*>(&cur_ver
),
345 reinterpret_cast<const uint8
*>(&cur_ver
) + sizeof(cur_ver
),
346 reinterpret_cast<const uint8
*>(&new_ver
), NULL
);
350 // Updates the version strings and numbers in all of |image_file|'s resources.
351 bool UpdateVersionIfMatch(const base::FilePath
& image_file
,
352 VisitResourceContext
* context
) {
354 context
->current_version_str
.size() < context
->new_version_str
.size()) {
359 uint32 flags
= base::File::FLAG_OPEN
| base::File::FLAG_READ
|
360 base::File::FLAG_WRITE
| base::File::FLAG_EXCLUSIVE_READ
|
361 base::File::FLAG_EXCLUSIVE_WRITE
;
362 base::File
file(image_file
, flags
);
363 // It turns out that the underlying CreateFile can fail due to unhelpful
364 // security software locking the newly created DLL. So add a few brief
365 // retries to help tests that use this pass on machines thusly encumbered.
367 while (!file
.IsValid() && retries
-- > 0) {
368 LOG(WARNING
) << "Failed to open \"" << image_file
.value() << "\"."
369 << " Retrying " << retries
<< " more times.";
371 file
.Initialize(image_file
, flags
);
374 if (file
.IsValid()) {
375 MappedFile image_mapping
;
376 if (image_mapping
.Initialize(file
.Pass())) {
377 base::win::PEImageAsData
image(
378 reinterpret_cast<HMODULE
>(image_mapping
.data()));
379 // PEImage class does not support other-architecture images.
380 if (image
.GetNTHeaders()->OptionalHeader
.Magic
==
381 IMAGE_NT_OPTIONAL_HDR_MAGIC
) {
382 result
= upgrade_test::EnumResources(
383 image
, &VisitResource
, reinterpret_cast<uintptr_t>(context
));
389 PLOG(DFATAL
) << "Failed to open \"" << image_file
.value() << "\"";
394 bool IncrementNewVersion(upgrade_test::Direction direction
,
395 VisitResourceContext
* ctx
) {
398 // Figure out a past or future version with the same string length as this one
399 // by decrementing or incrementing each component.
400 LONGLONG incrementer
= (direction
== upgrade_test::PREVIOUS_VERSION
? -1 : 1);
403 if (incrementer
== 0) {
404 LOG(DFATAL
) << "Improbable version at the cusp of complete rollover";
407 ctx
->new_version
.set_value(ctx
->current_version
.value() + incrementer
);
408 ctx
->new_version_str
= ctx
->new_version
.ToString();
410 } while (ctx
->new_version_str
.size() != ctx
->current_version_str
.size());
415 // Raises or lowers the version of all .exe and .dll files in |work_dir| as well
416 // as the |work-dir|\Chrome-bin\w.x.y.z directory. |original_version| and
417 // |new_version|, when non-NULL, are given the original and new version numbers
419 bool ApplyAlternateVersion(const base::FilePath
& work_dir
,
420 upgrade_test::Direction direction
,
421 std::wstring
* original_version
,
422 std::wstring
* new_version
) {
423 VisitResourceContext ctx
;
424 if (!GetSetupExeVersion(work_dir
, &ctx
.current_version
)) {
427 ctx
.current_version_str
= ctx
.current_version
.ToString();
429 if (!IncrementNewVersion(direction
, &ctx
)) {
433 // Modify all .dll and .exe files with the current version.
434 bool doing_great
= true;
435 base::FileEnumerator
all_files(work_dir
, true, base::FileEnumerator::FILES
);
437 base::FilePath file
= all_files
.Next();
441 std::wstring extension
= file
.Extension();
442 if (extension
== &kExtExe
[0] || extension
== &kExtDll
[0]) {
443 doing_great
= UpdateVersionIfMatch(file
, &ctx
);
445 } while (doing_great
);
447 // Change the versioned directory.
448 base::FilePath chrome_bin
= work_dir
.Append(&kChromeBin
[0]);
449 doing_great
= base::Move(chrome_bin
.Append(ctx
.current_version_str
),
450 chrome_bin
.Append(ctx
.new_version_str
));
453 // Report the version numbers if requested.
454 if (original_version
!= NULL
)
455 original_version
->assign(ctx
.current_version_str
);
456 if (new_version
!= NULL
)
457 new_version
->assign(ctx
.new_version_str
);
463 // Returns the path to the directory holding the 7za executable. By default, it
464 // is assumed that the test resides in the tree's output directory, so the
465 // relative path "..\..\third_party\lzma_sdk\Executable" is applied to the host
466 // executable's directory. This can be overridden with the --7za_path
467 // command-line switch.
468 base::FilePath
Get7zaPath() {
469 base::FilePath l7za_path
=
470 base::CommandLine::ForCurrentProcess()->GetSwitchValuePath(
472 if (l7za_path
.empty()) {
473 base::FilePath dir_exe
;
474 if (!PathService::Get(base::DIR_EXE
, &dir_exe
))
475 LOG(DFATAL
) << "Failed getting directory of host executable";
476 l7za_path
= dir_exe
.Append(&k7zaPathRelative
[0]);
481 bool CreateArchive(const base::FilePath
& output_file
,
482 const base::FilePath
& input_path
,
483 int compression_level
) {
484 DCHECK(compression_level
== 0 ||
485 compression_level
>= 1 && compression_level
<= 9 &&
486 (compression_level
& 0x01) != 0);
488 std::wstring
command_line(1, L
'"');
490 .append(Get7zaPath().Append(&k7zaExe
[0]).value())
491 .append(L
"\" a -bd -t7z \"")
492 .append(output_file
.value())
494 .append(input_path
.value())
496 .append(1, L
'0' + compression_level
);
498 if (!RunProcessAndWait(NULL
, command_line
, &exit_code
))
500 if (exit_code
!= 0) {
501 LOG(DFATAL
) << Get7zaPath().Append(&k7zaExe
[0]).value()
502 << " exited with code " << exit_code
503 << " while creating " << output_file
.value();
511 namespace upgrade_test
{
513 bool GenerateAlternateVersion(const base::FilePath
& original_installer_path
,
514 const base::FilePath
& target_path
,
516 std::wstring
* original_version
,
517 std::wstring
* new_version
) {
518 // Create a temporary directory in which we'll do our work.
519 ScopedTempDirectory work_dir
;
520 if (!work_dir
.Initialize())
523 // Copy the original mini_installer.
524 base::FilePath mini_installer
=
525 work_dir
.directory().Append(original_installer_path
.BaseName());
526 if (!base::CopyFile(original_installer_path
, mini_installer
)) {
527 LOG(DFATAL
) << "Failed copying \"" << original_installer_path
.value()
528 << "\" to \"" << mini_installer
.value() << "\"";
532 base::FilePath setup_ex_
= work_dir
.directory().Append(&kSetupEx_
[0]);
533 base::FilePath chrome_packed_7z
=
534 work_dir
.directory().Append(&kChromePacked7z
[0]);
535 // Load the original file and extract setup.ex_ and chrome.packed.7z
537 ResourceLoader resource_loader
;
538 std::pair
<const uint8
*, DWORD
> resource_data
;
540 if (!resource_loader
.Initialize(mini_installer
))
543 // Write out setup.ex_
544 if (!resource_loader
.Load(&kSetupEx_
[0], &kBl
[0], &resource_data
))
547 base::WriteFile(setup_ex_
,
548 reinterpret_cast<const char*>(resource_data
.first
),
549 static_cast<int>(resource_data
.second
));
550 if (written
!= resource_data
.second
) {
551 LOG(DFATAL
) << "Failed writing \"" << setup_ex_
.value() << "\"";
555 // Write out chrome.packed.7z
556 if (!resource_loader
.Load(&kChromePacked7z
[0], &kB7
[0], &resource_data
))
559 base::WriteFile(chrome_packed_7z
,
560 reinterpret_cast<const char*>(resource_data
.first
),
561 static_cast<int>(resource_data
.second
));
562 if (written
!= resource_data
.second
) {
563 LOG(DFATAL
) << "Failed writing \"" << chrome_packed_7z
.value() << "\"";
569 base::FilePath setup_exe
= setup_ex_
.ReplaceExtension(&kExe
[0]);
570 std::wstring command_line
;
571 command_line
.append(1, L
'"')
572 .append(&kExpandExe
[0])
574 .append(setup_ex_
.value())
576 .append(setup_exe
.value())
579 if (!RunProcessAndWait(NULL
, command_line
, &exit_code
))
581 if (exit_code
!= 0) {
582 LOG(DFATAL
) << &kExpandExe
[0] << " exited with code " << exit_code
;
586 // Unpack chrome.packed.7z
587 std::wstring chrome_7z_name
;
588 if (LzmaUtil::UnPackArchive(chrome_packed_7z
.value(),
589 work_dir
.directory().value(),
590 &chrome_7z_name
) != NO_ERROR
) {
591 LOG(DFATAL
) << "Failed unpacking \"" << chrome_packed_7z
.value() << "\"";
596 if (LzmaUtil::UnPackArchive(chrome_7z_name
, work_dir
.directory().value(),
598 LOG(DFATAL
) << "Failed unpacking \"" << chrome_7z_name
<< "\"";
602 // Get rid of intermediate files
603 base::FilePath
chrome_7z(chrome_7z_name
);
604 if (!base::DeleteFile(chrome_7z
, false) ||
605 !base::DeleteFile(chrome_packed_7z
, false) ||
606 !base::DeleteFile(setup_ex_
, false)) {
607 LOG(DFATAL
) << "Failed deleting intermediate files";
611 // Increment the version in all files.
612 ApplyAlternateVersion(work_dir
.directory(), direction
, original_version
,
615 // Pack up files into chrome.7z
616 if (!CreateArchive(chrome_7z
, work_dir
.directory().Append(&kChromeBin
[0]), 0))
619 // Compress chrome.7z into chrome.packed.7z
620 if (!CreateArchive(chrome_packed_7z
, chrome_7z
, 9))
623 // Compress setup.exe into setup.ex_
624 command_line
.assign(1, L
'"')
625 .append(&kMakeCab
[0])
626 .append(L
"\" /D CompressionType=LZX /L \"")
627 .append(work_dir
.directory().value())
629 .append(setup_exe
.value());
630 if (!RunProcessAndWait(NULL
, command_line
, &exit_code
))
632 if (exit_code
!= 0) {
633 LOG(DFATAL
) << &kMakeCab
[0] << " exited with code " << exit_code
;
637 // Replace the mini_installer's setup.ex_ and chrome.packed.7z resources.
638 ResourceUpdater updater
;
639 if (!updater
.Initialize(mini_installer
) ||
640 !updater
.Update(&kSetupEx_
[0], &kBl
[0],
641 MAKELANGID(LANG_ENGLISH
, SUBLANG_ENGLISH_US
),
643 !updater
.Update(&kChromePacked7z
[0], &kB7
[0],
644 MAKELANGID(LANG_ENGLISH
, SUBLANG_ENGLISH_US
),
650 // Finally, move the updated mini_installer into place.
651 return base::Move(mini_installer
, target_path
);
654 bool GenerateAlternatePEFileVersion(const base::FilePath
& original_file
,
655 const base::FilePath
& target_file
,
656 Direction direction
) {
657 VisitResourceContext ctx
;
658 if (!GetFileVersion(original_file
, &ctx
.current_version
)) {
659 LOG(DFATAL
) << "Failed reading version from \"" << original_file
.value()
663 ctx
.current_version_str
= ctx
.current_version
.ToString();
665 if (!IncrementNewVersion(direction
, &ctx
)) {
666 LOG(DFATAL
) << "Failed to increment version from \""
667 << original_file
.value() << "\"";
671 Version
new_version(base::UTF16ToASCII(ctx
.new_version_str
));
672 GenerateSpecificPEFileVersion(original_file
, target_file
, new_version
);
677 bool GenerateSpecificPEFileVersion(const base::FilePath
& original_file
,
678 const base::FilePath
& target_file
,
679 const Version
& version
) {
680 // First copy original_file to target_file.
681 if (!base::CopyFile(original_file
, target_file
)) {
682 LOG(DFATAL
) << "Failed copying \"" << original_file
.value()
683 << "\" to \"" << target_file
.value() << "\"";
687 VisitResourceContext ctx
;
688 if (!GetFileVersion(target_file
, &ctx
.current_version
)) {
689 LOG(DFATAL
) << "Failed reading version from \"" << target_file
.value()
693 ctx
.current_version_str
= ctx
.current_version
.ToString();
694 ctx
.new_version
= ChromeVersion::FromString(version
.GetString());
695 ctx
.new_version_str
= ctx
.new_version
.ToString();
697 return UpdateVersionIfMatch(target_file
, &ctx
);
700 } // namespace upgrade_test