Merge pull request #1 from Squirrel/recreate-core

Recreate Squirrel.Core
This commit is contained in:
Paul Betts
2014-07-29 10:06:54 +02:00
161 changed files with 114264 additions and 2 deletions

BIN
.nuget/NuGet.exe Normal file

Binary file not shown.

View File

@@ -10,10 +10,10 @@ This is Squirrel.Windows, rewritten to drop a lot of the things that caused the
* Squirrel.Windows allowed too much setup customization via installation hooks. This feature was super hard because installation hooks often had their own dependencies that blew up when we tried to load them. vNext loses this feature.
* Squirrel.Windows had a super complicated WiX-based installer that was an unholy nightmare. vNext replaces this with a single hardcoded C++ EXE whose goal is to display as little UI as possible.
* Squirrel.Windows had a super complicated WiX-based installer that was an unholy nightmare. vNext replaces this with a single hardcoded C++ EXE whose goal is to display as little UI as possible. [Installer Spec](https://github.com/Squirrel/Squirrel.Windows.Next/blob/recreate-core/specs/Installer.md)
* Squirrel.Windows was super IObservable-heavy, when the reality is that the vast majority of installer ops should just be synchronous. Ditch Rx completely and use async/await only when necessary.
* We didn't get anything but suffering out of IO abstractions. Kill 'em all.
* Squirrel got hella confused while walking the dependency tree by trying to detect which files in the NuGet package we were *actually* using (i.e. if you're a .NET 4.5 project, you could be using binaries from `Net20`, `Net35`, `Net45`, etc). Instead, write a Targets file which simply dumps the reference list to the output directory, and use that to inform which files should be in the final package.
* Squirrel got hella confused while walking the dependency tree by trying to detect which files in the NuGet package we were *actually* using (i.e. if you're a .NET 4.5 project, you could be using binaries from `Net20`, `Net35`, `Net45`, etc). Instead, write a Targets file which simply dumps the reference list to the output directory, and use that to inform which files should be in the final package. [Tools Spec](https://github.com/Squirrel/Squirrel.Windows.Next/blob/recreate-core/specs/Tools.md)

28
Squirrel.sln Normal file
View File

@@ -0,0 +1,28 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 2013
VisualStudioVersion = 12.0.30501.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel", "src\Squirrel.csproj", "{1436E22A-FE3C-4D68-9A85-9E74DF2E6A92}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Squirrel.Tests", "test\Squirrel.Tests.csproj", "{98AEB048-E27D-42F4-9440-505B7F78BAFD}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{1436E22A-FE3C-4D68-9A85-9E74DF2E6A92}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{1436E22A-FE3C-4D68-9A85-9E74DF2E6A92}.Debug|Any CPU.Build.0 = Debug|Any CPU
{1436E22A-FE3C-4D68-9A85-9E74DF2E6A92}.Release|Any CPU.ActiveCfg = Release|Any CPU
{1436E22A-FE3C-4D68-9A85-9E74DF2E6A92}.Release|Any CPU.Build.0 = Release|Any CPU
{98AEB048-E27D-42F4-9440-505B7F78BAFD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{98AEB048-E27D-42F4-9440-505B7F78BAFD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{98AEB048-E27D-42F4-9440-505B7F78BAFD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{98AEB048-E27D-42F4-9440-505B7F78BAFD}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

BIN
ext/Ionic.Zip.dll Normal file

Binary file not shown.

18151
ext/Ionic.Zip.xml Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,153 @@
# Client-side Library
To be able to meet the specifications of the "updates" section of the README
(especially the bits about 'No Reboots', 'Updates should be applied while the
app is running'), we have to be a bit more clever than "Stuff everything in a
folder, hit go".
### How can you replace DLLs while they're loaded? Impossible!
You can't. So, how can you do it? The basic trick that ClickOnce uses is, you
have a folder of EXEs and DLLs, and an Application Shortcut. When ClickOnce
goes to update its stuff, it builds a completely *new* folder of binaries,
then the last thing it does is rewrite the app shortcut to point to the new
folder.
So, to that end, the installation root really only needs to consist of two
folders:
```
\packages
MyCoolApp-1.0.nupkg
MyCoolApp-1.1-delta.nupkg
MyCoolApp-1.1.nupkg ## Generated from 1.0+1.1-delta
\app-[version]
```
Packages is effectively immutable, it simply consists of the packages we've
downloaded. This means however, that we need write-access to our own install
directory - this is fine for per-user installs, but if the user has installed
to Program Files, we'll need to come up with another solution. And that
solution is, "Only support per-user installs".
## The Update process, from start to finish
### Syncing the packages directory
The first thing that the Squirrel client will do to start the updates process, is
download the remote version of "Releases". Comparing this file to the Releases
file on disk will tell us whether an update is available.
Determining whether to use the delta packages or not will depend on the
download size - the updater will take the smaller of "latest full package" vs.
"Sum of all delta packages between current and latest". The updater makes a
choice, then fetches down all the files and checks them against the SHA1s in
the Releases file.
If the installer decided to do a Delta update, it will then use the Delta
updates against the existing Full package to build a new Full package.
### Installing a full update
Since we've done the prep work to create a new NuGet package from the deltas,
the actual update process only has to deal with full NuGet packages. This is
as simple as:
1. Extract the NuGet package to a temp dir
1. Move lib\net40 to \app-[newversion]
1. Rewrite the shortcut to point to \app-[newversion]
On next startup, we blow away \app-[version] since it's now the previous
version of the code.
### What do we do on Setup? (Bootstrapping)
Since the WiX setup application is too dumb to setup our default directory, in
order to simplify trying to bootstrap our app directory, we'll just recreate
it. This is some wasted bandwidth, but oh well. If the packages or app root
doesn't actually exist, we'll download the latest full release and set up the
app.
### Client-side API
Referencing Squirrel.Client.dll, `UpdateManager` is all the app dev needs to use.
UpdateManager
UpdateInfo CheckForUpdates()
UpdateInfo DownloadUpdate()
List<string> ApplyUpdates()
`UpdateInfo` contains information about pending updates if there is
any, and is null if there isn't.
UpdateInfo
ReleaseEntry CurrentlyInstalledVersion
ReleaseEntry FutureReleaseEntry
IEnumerable<ReleaseEntry> ReleasesToApply
And `ReleaseEntry` contains the specifics of each release:
ReleaseEntry
string SHA1
string Filename
long Filesize
bool IsDelta
## Applying Updates
#### A note about Reactive Extensions
Squirrel uses Reactive Extensions (Rx) heavily as the process necessary to
retrieve, download and apply updates is best done asynchronously. If you
are using the `Microsoft.Bcl.Async` package (which Squirrel also uses) you
can combine the Rx APIs with the TPL async/await keywords, for maximum
simplicity.
### Check yourself
First, check the location where your application updates are hosted:
```
var updateManager = new UpdateManager(@"C:\Users\brendanforster\Desktop\TestApp",
"TestApp",
FrameworkVersion.Net40);
var updateInfo = await updateManager.CheckForUpdate();
if (updateInfo == null) {
Console.WriteLine("No updates found");
} else if (!info.ReleasesToApply.Any()) {
Console.WriteLine("You're up to date!");
} else {
var latest = info.ReleasesToApply.MaxBy(x => x.Version).First();
Console.WriteLine("You can update to {0}", latest.Version);
}
```
Depending on the result you get from this operation, you might:
- not detect any updates
- be on the latest version
- have one or more versions to apply
### Fetch all the Updates
The result from `CheckForUpdates` will contain a list of releases to apply to
your current application.
That result becomes the input to `DownloadReleases`:
```
var releases = updateInfo.ReleasesToApply;
await updateManager.DownloadReleases(releases);
```
### Apply dem Updates
And lastly, once those updates have been downloaded, tell Squirrel to apply them:
```
var results = await updateManager.ApplyReleases(downloadedUpdateInfo);
updateManager.Dispose(); // don't forget to tidy up after yourself
```

95
specs/Implementation.md Normal file
View File

@@ -0,0 +1,95 @@
# Implementation
## Major Pieces
We need:
- A client library, which includes the core update logic
- An executable / PowerShell script to implement `New-Release`
- The actual Setup.exe that Create-Release hacks up, as well as any related
implementation (WiX stuff?) that we need.
## Production / "Server Side"
### The tricky part
Ironically, the difficulty of using NuGet packages as a distribution container
for your app, is *if your app uses NuGet*. This is because NuGet (with good
reason!) packages the *list* of dependencies, not the actual binaries. So, if
we were to try to use the NuGet package of the App directly, we'd be missing a
bunch of DLLs.
So, we need an application that can *flatten* a NuGet dependency tree and
repack the package with all the DLLs. While this is a lot of steps, it's
actually pretty straightforward:
1. Extract the App's NuGet package to a temp directory.
1. Walk the list of dependencies. For each dependency, extract it on top of
the temp directory (i.e. so that its `lib/*` ends up in the App's dir)
1. Recursively do the same thing (i.e. recurse down the dependency tree)
1. Edit the root NuGet package XML and remove all its explicit dependencies.
This is kind of the moral equivalent of the Rails Gem "vendor freeze" I guess.
### Delta Packages
Now, once we've got a full package, we need to generate a Delta package. To do
this, we'll replace all the DLL/EXEs in the NuGet packages with bsdiff files.
[bspatch/bsdiff](http://code.logos.com/blog/2010/12/binary_patching_with_bsdiff.html)
is a mostly efficient algorithm for calculating diffs between binary files
(especially Native binaries, but it works well for .NET ones too), and a way
to apply them.
So, this is pretty easy:
1. Extract the previous NuGet package
1. Extract the current NuGet package
1. Replace every EXE/DLL with the bsdiff. So, `lib\net40\MyCoolApp.exe`
becomes `lib\net40\MyCoolApp.exe.diff`. Create a file that contains a SHA1
of the expected resulting file and its filesize called
`lib\net40\MyCoolApp.exe.shasum`
1. New DLLs in current get put in verbatim
1. Zip it back up
The .shasum file has the same format as the Releases file described in the
"'Latest' Pointer" section, except that it will only have one entry.
So now we've got all of the *metadata* of the original package, just none of
its *contents*. To get the final package, we do the following:
1. Take the previous version, expand it out
1. Take the delta version, do the same
1. For each DLL in the previous package, we bspatch it, then check the shasum
file to ensure we created the correct resulting file
1. If we find a DLL in the new package, just copy it over
1. If we can't find a bspatch for a file, nuke it (it doesn't exist in the new
rev)
1. Zip it back up
### ChangeLogs / Release Notes
To write release notes for each release, we're going to reuse the
`<ReleaseNotes>` NuSpec element. However, we're going to standard that you
can write Markdown in this element, and as part of generating a flattened
package, we will render this Markdown as HTML.
### "Latest" Pointer
One of the last things we do before finishing `Create-Release` is that we
write out a simple "Releases" file alongside the flattened and Delta NuGet
packages. This is a text file that has the name of all of the release package
filenames in the folder in release order (i.e. oldest at top, newest at
bottom), along with the SHA1 hashes of their contents and their file sizes.
So, something like:
```
94689fede03fed7ab59c24337673a27837f0c3ec MyCoolApp-1.0.nupkg 1004502
3a2eadd15dd984e4559f2b4d790ec8badaeb6a39 MyCoolApp-1.1.nupkg 1040561
14db31d2647c6d2284882a2e101924a9c409ee67 MyCoolApp-1.1-delta.nupkg 80396
```
This format has a number of advantages - it's dead simple, yet enables us to
check for package corruption, as well as makes it efficient to determine what
to do if a user gets multiple versions behind (i.e. whether it's worth it to
download all of the delta packages to catch them up, or to just download the
latest full package)

30
specs/Installer.md Normal file
View File

@@ -0,0 +1,30 @@
# Installer
The installer consists of two parts: `Setup.exe` (C++ bootstrapper) and `Update.exe` (C# Squirrel Client). There are several main design goals of the installer:
* Run as quickly as possible, with as little user interface interaction as possible. The faster we can get into the application, the better. An ideal install experience is that once `Setup.exe` gets clicked, within 3sec the application is running on the user's machine. Double-clicking `Setup.exe` should feel like clicking the app shortcut.
* `Setup.exe` should be written such that it does as little work as possible, because C++.
* Running an older `Setup.exe` should simply execute the current app.
* Support installation of non-C# applications
## Setup.exe
Setup.exe does the following operations:
1. Determines if the .NET Framework is installed
1. If not, relaunches itself with `/installfx45`, which opens a progress dialog which downloads the .NET Framework and silently invokes it.
1. Extract `Update.exe` and `AppName-full.nupkg` to `%LocalAppData%\Squirrel\Temp` which are embedded as resources.
1. Execute `Update.exe` with the `/install` switch, and apply any switches that were applied to Setup.exe
1. Nuke the extracted temporary files from step 3.
## Update.exe
Update.exe is a generic client for Squirrel which supports several operations:
* `/install [File.nupkg] [/silent]` - Install the NuPkg file given (or any NuPkg files in the same directory as itself), and launch their applications. If `/silent` is given, don't launch anything. Copy `Update.exe` to the application root directory. Install also writes an entry in Programs and Features which will invoke `/uninstall`.
* `/uninstall` - Completely uninstall the application associated with the directory in which `Update.exe` resides.
* `/download URL` - Check for updates from the given URL and write information about available versions to standard output in JSON format.
* `/update` - Updates the application to the latest version of the files in the packages directory associated with the directory in which `Update.exe` resides.

23
specs/Scenarios.md Normal file
View File

@@ -0,0 +1,23 @@
## Scenarios
#### Production
I'm a developer with a WPF application. I have *zero* way to distribute my application at the moment. I go to NuGet and install the Squirrel client library.
Now, I want to publish a release. To do so, I pop into the PowerShell Console and type `New-Release`. What does this do? It:
* Creates a NuGet package of my app (i.e. via shelling out to NuGet.exe or w/e)
* It puts the package in a special "Releases" directory of my solution (along with a delta package for updates)
* It also creates a Setup.exe that I can distribute to people
* Can also transform `changelog.md` to `changelog.html` using the bundled Markdown library that ships with Squirrel
I've created a new release. Now, I want to share it with the world! I upload the contents of my Releases directory verbatim to the web via S3 / FTP / whatever.
In my app, I call `bool UpdateManager.CheckForUpdates("http://mycoolsite.com/releases/")` - similar to ClickOnce API but not awful. The library helps me check for updates, get the ChangeLog HTML to render, and if I'm really lazy, I can just call `UpdateManager.ShowUpdateNotification()` and get a stock WPF dialog walking the user through the upgrade. For production applications, I get the information I need to create my own update experience (yet I don't have to do any of the actual heavy lifting).
When I call `UpdateManager.Upgrade()`, the application does the update in the background, without disturbing the user at all - the next time the app restarts, it's the new version.
#### Users
I click on a link, and within seconds my application starts. No install experience, no dialogs, no UAC.

20
specs/Tools.md Normal file
View File

@@ -0,0 +1,20 @@
## Scenarios
At the end of the day, here's how a developer will use Squirrel:
1. Add the **Squirrel** package to your application
1. As part of the install for Squirrel, NuGet Package Build is enabled in the csproj file
1. The user edits the generated `.nuspec` to specify some details about their app
1. From the NuGet package console, run `New-Release` - this builds the world, and you end up with a `$SolutionDir/Releases/ASSEMBLYNAME` folder that has both a Squirrel release package as well as a `Setup.exe`
## How does this work:
As part of adding Squirrel to your application, a `targets` file gets added to your csproj file. This targets file dumps all of the references in your application to the output directory in a simple text file, as well as a list of files marked as content.
Calling `New-Release` results in this process being kicked off:
1. Call `$DTE` to build the current project, including the NuGet packages
1. For the current project, run `CreateReleasePackage.exe` to build release and delta packages.
1. Create a Zip file consisting of `update.exe` and the latest full release from `Releases`.
1. Using Win32 API Abuse™, put that into `setup.exe`, a C++ bootstrapper application whose sole goal is to download .NET 4.5, install it, then run update.exe
1. Copy that to the Releases folder.

899
src/BinaryPatchUtility.cs Normal file
View File

@@ -0,0 +1,899 @@
using System;
using System.IO;
using Ionic.BZip2;
// Adapted from https://github.com/LogosBible/bsdiff.net/blob/master/src/bsdiff/BinaryPatchUtility.cs
namespace Squirrel
{
/*
The original bsdiff.c source code (http://www.daemonology.net/bsdiff/) is
distributed under the following license:
Copyright 2003-2005 Colin Percival
All rights reserved
Redistribution and use in source and binary forms, with or without
modification, are permitted providing that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
class BinaryPatchUtility
{
/// <summary>
/// Creates a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) that can be used
/// (by <see cref="Apply"/>) to transform <paramref name="oldData"/> into <paramref name="newData"/>.
/// </summary>
/// <param name="oldData">The original binary data.</param>
/// <param name="newData">The new binary data.</param>
/// <param name="output">A <see cref="Stream"/> to which the patch will be written.</param>
public static void Create(byte[] oldData, byte[] newData, Stream output)
{
// check arguments
if (oldData == null)
throw new ArgumentNullException("oldData");
if (newData == null)
throw new ArgumentNullException("newData");
if (output == null)
throw new ArgumentNullException("output");
if (!output.CanSeek)
throw new ArgumentException("Output stream must be seekable.", "output");
if (!output.CanWrite)
throw new ArgumentException("Output stream must be writable.", "output");
/* Header is
0 8 "BSDIFF40"
8 8 length of bzip2ed ctrl block
16 8 length of bzip2ed diff block
24 8 length of new file */
/* File is
0 32 Header
32 ?? Bzip2ed ctrl block
?? ?? Bzip2ed diff block
?? ?? Bzip2ed extra block */
byte[] header = new byte[c_headerSize];
WriteInt64(c_fileSignature, header, 0); // "BSDIFF40"
WriteInt64(0, header, 8);
WriteInt64(0, header, 16);
WriteInt64(newData.Length, header, 24);
long startPosition = output.Position;
output.Write(header, 0, header.Length);
int[] I = SuffixSort(oldData);
byte[] db = new byte[newData.Length + 1];
byte[] eb = new byte[newData.Length + 1];
int dblen = 0;
int eblen = 0;
using (WrappingStream wrappingStream = new WrappingStream(output, Ownership.None))
using (BZip2OutputStream bz2Stream = new BZip2OutputStream(wrappingStream))
{
// compute the differences, writing ctrl as we go
int scan = 0;
int pos = 0;
int len = 0;
int lastscan = 0;
int lastpos = 0;
int lastoffset = 0;
while (scan < newData.Length)
{
int oldscore = 0;
for (int scsc = scan += len; scan < newData.Length; scan++)
{
len = Search(I, oldData, newData, scan, 0, oldData.Length, out pos);
for (; scsc < scan + len; scsc++)
{
if ((scsc + lastoffset < oldData.Length) && (oldData[scsc + lastoffset] == newData[scsc]))
oldscore++;
}
if ((len == oldscore && len != 0) || (len > oldscore + 8))
break;
if ((scan + lastoffset < oldData.Length) && (oldData[scan + lastoffset] == newData[scan]))
oldscore--;
}
if (len != oldscore || scan == newData.Length)
{
int s = 0;
int sf = 0;
int lenf = 0;
for (int i = 0; (lastscan + i < scan) && (lastpos + i < oldData.Length); )
{
if (oldData[lastpos + i] == newData[lastscan + i])
s++;
i++;
if (s * 2 - i > sf * 2 - lenf)
{
sf = s;
lenf = i;
}
}
int lenb = 0;
if (scan < newData.Length)
{
s = 0;
int sb = 0;
for (int i = 1; (scan >= lastscan + i) && (pos >= i); i++)
{
if (oldData[pos - i] == newData[scan - i])
s++;
if (s * 2 - i > sb * 2 - lenb)
{
sb = s;
lenb = i;
}
}
}
if (lastscan + lenf > scan - lenb)
{
int overlap = (lastscan + lenf) - (scan - lenb);
s = 0;
int ss = 0;
int lens = 0;
for (int i = 0; i < overlap; i++)
{
if (newData[lastscan + lenf - overlap + i] == oldData[lastpos + lenf - overlap + i])
s++;
if (newData[scan - lenb + i] == oldData[pos - lenb + i])
s--;
if (s > ss)
{
ss = s;
lens = i + 1;
}
}
lenf += lens - overlap;
lenb -= lens;
}
for (int i = 0; i < lenf; i++)
db[dblen + i] = (byte)(newData[lastscan + i] - oldData[lastpos + i]);
for (int i = 0; i < (scan - lenb) - (lastscan + lenf); i++)
eb[eblen + i] = newData[lastscan + lenf + i];
dblen += lenf;
eblen += (scan - lenb) - (lastscan + lenf);
byte[] buf = new byte[8];
WriteInt64(lenf, buf, 0);
bz2Stream.Write(buf, 0, 8);
WriteInt64((scan - lenb) - (lastscan + lenf), buf, 0);
bz2Stream.Write(buf, 0, 8);
WriteInt64((pos - lenb) - (lastpos + lenf), buf, 0);
bz2Stream.Write(buf, 0, 8);
lastscan = scan - lenb;
lastpos = pos - lenb;
lastoffset = pos - scan;
}
}
}
// compute size of compressed ctrl data
long controlEndPosition = output.Position;
WriteInt64(controlEndPosition - startPosition - c_headerSize, header, 8);
// write compressed diff data
using (WrappingStream wrappingStream = new WrappingStream(output, Ownership.None))
using (BZip2OutputStream bz2Stream = new BZip2OutputStream(wrappingStream))
{
bz2Stream.Write(db, 0, dblen);
}
// compute size of compressed diff data
long diffEndPosition = output.Position;
WriteInt64(diffEndPosition - controlEndPosition, header, 16);
// write compressed extra data
using (WrappingStream wrappingStream = new WrappingStream(output, Ownership.None))
using (BZip2OutputStream bz2Stream = new BZip2OutputStream(wrappingStream))
{
bz2Stream.Write(eb, 0, eblen);
}
// seek to the beginning, write the header, then seek back to end
long endPosition = output.Position;
output.Position = startPosition;
output.Write(header, 0, header.Length);
output.Position = endPosition;
}
/// <summary>
/// Applies a binary patch (in <a href="http://www.daemonology.net/bsdiff/">bsdiff</a> format) to the data in
/// <paramref name="input"/> and writes the results of patching to <paramref name="output"/>.
/// </summary>
/// <param name="input">A <see cref="Stream"/> containing the input data.</param>
/// <param name="openPatchStream">A func that can open a <see cref="Stream"/> positioned at the start of the patch data.
/// This stream must support reading and seeking, and <paramref name="openPatchStream"/> must allow multiple streams on
/// the patch to be opened concurrently.</param>
/// <param name="output">A <see cref="Stream"/> to which the patched data is written.</param>
public static void Apply(Stream input, Func<Stream> openPatchStream, Stream output)
{
// check arguments
if (input == null)
throw new ArgumentNullException("input");
if (openPatchStream == null)
throw new ArgumentNullException("openPatchStream");
if (output == null)
throw new ArgumentNullException("output");
/*
File format:
0 8 "BSDIFF40"
8 8 X
16 8 Y
24 8 sizeof(newfile)
32 X bzip2(control block)
32+X Y bzip2(diff block)
32+X+Y ??? bzip2(extra block)
with control block a set of triples (x,y,z) meaning "add x bytes
from oldfile to x bytes from the diff block; copy y bytes from the
extra block; seek forwards in oldfile by z bytes".
*/
// read header
long controlLength, diffLength, newSize;
using (Stream patchStream = openPatchStream())
{
// check patch stream capabilities
if (!patchStream.CanRead)
throw new ArgumentException("Patch stream must be readable.", "openPatchStream");
if (!patchStream.CanSeek)
throw new ArgumentException("Patch stream must be seekable.", "openPatchStream");
byte[] header = patchStream.ReadExactly(c_headerSize);
// check for appropriate magic
long signature = ReadInt64(header, 0);
if (signature != c_fileSignature)
throw new InvalidOperationException("Corrupt patch.");
// read lengths from header
controlLength = ReadInt64(header, 8);
diffLength = ReadInt64(header, 16);
newSize = ReadInt64(header, 24);
if (controlLength < 0 || diffLength < 0 || newSize < 0)
throw new InvalidOperationException("Corrupt patch.");
}
// preallocate buffers for reading and writing
const int c_bufferSize = 1048576;
byte[] newData = new byte[c_bufferSize];
byte[] oldData = new byte[c_bufferSize];
// prepare to read three parts of the patch in parallel
using (Stream compressedControlStream = openPatchStream())
using (Stream compressedDiffStream = openPatchStream())
using (Stream compressedExtraStream = openPatchStream())
{
// seek to the start of each part
compressedControlStream.Seek(c_headerSize, SeekOrigin.Current);
compressedDiffStream.Seek(c_headerSize + controlLength, SeekOrigin.Current);
compressedExtraStream.Seek(c_headerSize + controlLength + diffLength, SeekOrigin.Current);
// decompress each part (to read it)
using (BZip2InputStream controlStream = new BZip2InputStream(compressedControlStream))
using (BZip2InputStream diffStream = new BZip2InputStream(compressedDiffStream))
using (BZip2InputStream extraStream = new BZip2InputStream(compressedExtraStream))
{
long[] control = new long[3];
byte[] buffer = new byte[8];
int oldPosition = 0;
int newPosition = 0;
while (newPosition < newSize)
{
// read control data
for (int i = 0; i < 3; i++)
{
controlStream.ReadExactly(buffer, 0, 8);
control[i] = ReadInt64(buffer, 0);
}
// sanity-check
if (newPosition + control[0] > newSize)
throw new InvalidOperationException("Corrupt patch.");
// seek old file to the position that the new data is diffed against
input.Position = oldPosition;
int bytesToCopy = (int)control[0];
while (bytesToCopy > 0)
{
int actualBytesToCopy = Math.Min(bytesToCopy, c_bufferSize);
// read diff string
diffStream.ReadExactly(newData, 0, actualBytesToCopy);
// add old data to diff string
int availableInputBytes = Math.Min(actualBytesToCopy, (int)(input.Length - input.Position));
input.ReadExactly(oldData, 0, availableInputBytes);
for (int index = 0; index < availableInputBytes; index++)
newData[index] += oldData[index];
output.Write(newData, 0, actualBytesToCopy);
// adjust counters
newPosition += actualBytesToCopy;
oldPosition += actualBytesToCopy;
bytesToCopy -= actualBytesToCopy;
}
// sanity-check
if (newPosition + control[1] > newSize)
throw new InvalidOperationException("Corrupt patch.");
// read extra string
bytesToCopy = (int)control[1];
while (bytesToCopy > 0)
{
int actualBytesToCopy = Math.Min(bytesToCopy, c_bufferSize);
extraStream.ReadExactly(newData, 0, actualBytesToCopy);
output.Write(newData, 0, actualBytesToCopy);
newPosition += actualBytesToCopy;
bytesToCopy -= actualBytesToCopy;
}
// adjust position
oldPosition = (int)(oldPosition + control[2]);
}
}
}
}
private static int CompareBytes(byte[] left, int leftOffset, byte[] right, int rightOffset)
{
for (int index = 0; index < left.Length - leftOffset && index < right.Length - rightOffset; index++)
{
int diff = left[index + leftOffset] - right[index + rightOffset];
if (diff != 0)
return diff;
}
return 0;
}
private static int MatchLength(byte[] oldData, int oldOffset, byte[] newData, int newOffset)
{
int i;
for (i = 0; i < oldData.Length - oldOffset && i < newData.Length - newOffset; i++)
{
if (oldData[i + oldOffset] != newData[i + newOffset])
break;
}
return i;
}
private static int Search(int[] I, byte[] oldData, byte[] newData, int newOffset, int start, int end, out int pos)
{
if (end - start < 2)
{
int startLength = MatchLength(oldData, I[start], newData, newOffset);
int endLength = MatchLength(oldData, I[end], newData, newOffset);
if (startLength > endLength)
{
pos = I[start];
return startLength;
}
else
{
pos = I[end];
return endLength;
}
}
else
{
int midPoint = start + (end - start) / 2;
return CompareBytes(oldData, I[midPoint], newData, newOffset) < 0 ?
Search(I, oldData, newData, newOffset, midPoint, end, out pos) :
Search(I, oldData, newData, newOffset, start, midPoint, out pos);
}
}
private static void Split(int[] I, int[] v, int start, int len, int h)
{
if (len < 16)
{
int j;
for (int k = start; k < start + len; k += j)
{
j = 1;
int x = v[I[k] + h];
for (int i = 1; k + i < start + len; i++)
{
if (v[I[k + i] + h] < x)
{
x = v[I[k + i] + h];
j = 0;
}
if (v[I[k + i] + h] == x)
{
Swap(ref I[k + j], ref I[k + i]);
j++;
}
}
for (int i = 0; i < j; i++)
v[I[k + i]] = k + j - 1;
if (j == 1)
I[k] = -1;
}
}
else
{
int x = v[I[start + len / 2] + h];
int jj = 0;
int kk = 0;
for (int i2 = start; i2 < start + len; i2++)
{
if (v[I[i2] + h] < x)
jj++;
if (v[I[i2] + h] == x)
kk++;
}
jj += start;
kk += jj;
int i = start;
int j = 0;
int k = 0;
while (i < jj)
{
if (v[I[i] + h] < x)
{
i++;
}
else if (v[I[i] + h] == x)
{
Swap(ref I[i], ref I[jj + j]);
j++;
}
else
{
Swap(ref I[i], ref I[kk + k]);
k++;
}
}
while (jj + j < kk)
{
if (v[I[jj + j] + h] == x)
{
j++;
}
else
{
Swap(ref I[jj + j], ref I[kk + k]);
k++;
}
}
if (jj > start)
Split(I, v, start, jj - start, h);
for (i = 0; i < kk - jj; i++)
v[I[jj + i]] = kk - 1;
if (jj == kk - 1)
I[jj] = -1;
if (start + len > kk)
Split(I, v, kk, start + len - kk, h);
}
}
private static int[] SuffixSort(byte[] oldData)
{
int[] buckets = new int[256];
foreach (byte oldByte in oldData)
buckets[oldByte]++;
for (int i = 1; i < 256; i++)
buckets[i] += buckets[i - 1];
for (int i = 255; i > 0; i--)
buckets[i] = buckets[i - 1];
buckets[0] = 0;
int[] I = new int[oldData.Length + 1];
for (int i = 0; i < oldData.Length; i++)
I[++buckets[oldData[i]]] = i;
int[] v = new int[oldData.Length + 1];
for (int i = 0; i < oldData.Length; i++)
v[i] = buckets[oldData[i]];
for (int i = 1; i < 256; i++)
{
if (buckets[i] == buckets[i - 1] + 1)
I[buckets[i]] = -1;
}
I[0] = -1;
for (int h = 1; I[0] != -(oldData.Length + 1); h += h)
{
int len = 0;
int i = 0;
while (i < oldData.Length + 1)
{
if (I[i] < 0)
{
len -= I[i];
i -= I[i];
}
else
{
if (len != 0)
I[i - len] = -len;
len = v[I[i]] + 1 - i;
Split(I, v, i, len, h);
i += len;
len = 0;
}
}
if (len != 0)
I[i - len] = -len;
}
for (int i = 0; i < oldData.Length + 1; i++)
I[v[i]] = i;
return I;
}
private static void Swap(ref int first, ref int second)
{
int temp = first;
first = second;
second = temp;
}
private static long ReadInt64(byte[] buf, int offset)
{
long value = buf[offset + 7] & 0x7F;
for (int index = 6; index >= 0; index--)
{
value *= 256;
value += buf[offset + index];
}
if ((buf[offset + 7] & 0x80) != 0)
value = -value;
return value;
}
private static void WriteInt64(long value, byte[] buf, int offset)
{
long valueToWrite = value < 0 ? -value : value;
for (int byteIndex = 0; byteIndex < 8; byteIndex++)
{
buf[offset + byteIndex] = (byte)(valueToWrite % 256);
valueToWrite -= buf[offset + byteIndex];
valueToWrite /= 256;
}
if (value < 0)
buf[offset + 7] |= 0x80;
}
const long c_fileSignature = 0x3034464649445342L;
const int c_headerSize = 32;
}
/// <summary>
/// A <see cref="Stream"/> that wraps another stream. One major feature of <see cref="WrappingStream"/> is that it does not dispose the
/// underlying stream when it is disposed if Ownership.None is used; this is useful when using classes such as <see cref="BinaryReader"/> and
/// <see cref="System.Security.Cryptography.CryptoStream"/> that take ownership of the stream passed to their constructors.
/// </summary>
/// <remarks>See <a href="http://code.logos.com/blog/2009/05/wrappingstream_implementation.html">WrappingStream Implementation</a>.</remarks>
public class WrappingStream : Stream
{
/// <summary>
/// Initializes a new instance of the <see cref="WrappingStream"/> class.
/// </summary>
/// <param name="streamBase">The wrapped stream.</param>
/// <param name="ownership">Use Owns if the wrapped stream should be disposed when this stream is disposed.</param>
public WrappingStream(Stream streamBase, Ownership ownership)
{
// check parameters
if (streamBase == null)
throw new ArgumentNullException("streamBase");
m_streamBase = streamBase;
m_ownership = ownership;
}
/// <summary>
/// Gets a value indicating whether the current stream supports reading.
/// </summary>
/// <returns><c>true</c> if the stream supports reading; otherwise, <c>false</c>.</returns>
public override bool CanRead
{
get { return m_streamBase == null ? false : m_streamBase.CanRead; }
}
/// <summary>
/// Gets a value indicating whether the current stream supports seeking.
/// </summary>
/// <returns><c>true</c> if the stream supports seeking; otherwise, <c>false</c>.</returns>
public override bool CanSeek
{
get { return m_streamBase == null ? false : m_streamBase.CanSeek; }
}
/// <summary>
/// Gets a value indicating whether the current stream supports writing.
/// </summary>
/// <returns><c>true</c> if the stream supports writing; otherwise, <c>false</c>.</returns>
public override bool CanWrite
{
get { return m_streamBase == null ? false : m_streamBase.CanWrite; }
}
/// <summary>
/// Gets the length in bytes of the stream.
/// </summary>
public override long Length
{
get { ThrowIfDisposed(); return m_streamBase.Length; }
}
/// <summary>
/// Gets or sets the position within the current stream.
/// </summary>
public override long Position
{
get { ThrowIfDisposed(); return m_streamBase.Position; }
set { ThrowIfDisposed(); m_streamBase.Position = value; }
}
/// <summary>
/// Begins an asynchronous read operation.
/// </summary>
public override IAsyncResult BeginRead(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{
ThrowIfDisposed();
return m_streamBase.BeginRead(buffer, offset, count, callback, state);
}
/// <summary>
/// Begins an asynchronous write operation.
/// </summary>
public override IAsyncResult BeginWrite(byte[] buffer, int offset, int count, AsyncCallback callback, object state)
{
ThrowIfDisposed();
return m_streamBase.BeginWrite(buffer, offset, count, callback, state);
}
/// <summary>
/// Waits for the pending asynchronous read to complete.
/// </summary>
public override int EndRead(IAsyncResult asyncResult)
{
ThrowIfDisposed();
return m_streamBase.EndRead(asyncResult);
}
/// <summary>
/// Ends an asynchronous write operation.
/// </summary>
public override void EndWrite(IAsyncResult asyncResult)
{
ThrowIfDisposed();
m_streamBase.EndWrite(asyncResult);
}
/// <summary>
/// Clears all buffers for this stream and causes any buffered data to be written to the underlying device.
/// </summary>
public override void Flush()
{
ThrowIfDisposed();
m_streamBase.Flush();
}
/// <summary>
/// Reads a sequence of bytes from the current stream and advances the position
/// within the stream by the number of bytes read.
/// </summary>
public override int Read(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
return m_streamBase.Read(buffer, offset, count);
}
/// <summary>
/// Reads a byte from the stream and advances the position within the stream by one byte, or returns -1 if at the end of the stream.
/// </summary>
public override int ReadByte()
{
ThrowIfDisposed();
return m_streamBase.ReadByte();
}
/// <summary>
/// Sets the position within the current stream.
/// </summary>
/// <param name="offset">A byte offset relative to the <paramref name="origin"/> parameter.</param>
/// <param name="origin">A value of type <see cref="T:System.IO.SeekOrigin"/> indicating the reference point used to obtain the new position.</param>
/// <returns>The new position within the current stream.</returns>
public override long Seek(long offset, SeekOrigin origin)
{
ThrowIfDisposed();
return m_streamBase.Seek(offset, origin);
}
/// <summary>
/// Sets the length of the current stream.
/// </summary>
/// <param name="value">The desired length of the current stream in bytes.</param>
public override void SetLength(long value)
{
ThrowIfDisposed();
m_streamBase.SetLength(value);
}
/// <summary>
/// Writes a sequence of bytes to the current stream and advances the current position
/// within this stream by the number of bytes written.
/// </summary>
public override void Write(byte[] buffer, int offset, int count)
{
ThrowIfDisposed();
m_streamBase.Write(buffer, offset, count);
}
/// <summary>
/// Writes a byte to the current position in the stream and advances the position within the stream by one byte.
/// </summary>
public override void WriteByte(byte value)
{
ThrowIfDisposed();
m_streamBase.WriteByte(value);
}
/// <summary>
/// Gets the wrapped stream.
/// </summary>
/// <value>The wrapped stream.</value>
protected Stream WrappedStream
{
get { return m_streamBase; }
}
/// <summary>
/// Releases the unmanaged resources used by the <see cref="WrappingStream"/> and optionally releases the managed resources.
/// </summary>
/// <param name="disposing">true to release both managed and unmanaged resources; false to release only unmanaged resources.</param>
protected override void Dispose(bool disposing)
{
try
{
// doesn't close the base stream, but just prevents access to it through this WrappingStream
if (disposing)
{
if (m_streamBase != null && m_ownership == Ownership.Owns)
m_streamBase.Dispose();
m_streamBase = null;
}
}
finally
{
base.Dispose(disposing);
}
}
private void ThrowIfDisposed()
{
// throws an ObjectDisposedException if this object has been disposed
if (m_streamBase == null)
throw new ObjectDisposedException(GetType().Name);
}
Stream m_streamBase;
readonly Ownership m_ownership;
}
/// <summary>
/// Indicates whether an object takes ownership of an item.
/// </summary>
public enum Ownership
{
/// <summary>
/// The object does not own this item.
/// </summary>
None,
/// <summary>
/// The object owns this item, and is responsible for releasing it.
/// </summary>
Owns
}
/// <summary>
/// Provides helper methods for working with <see cref="Stream"/>.
/// </summary>
public static class StreamUtility
{
/// <summary>
/// Reads exactly <paramref name="count"/> bytes from <paramref name="stream"/>.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="count">The count of bytes to read.</param>
/// <returns>A new byte array containing the data read from the stream.</returns>
public static byte[] ReadExactly(this Stream stream, int count)
{
if (count < 0)
throw new ArgumentOutOfRangeException("count");
byte[] buffer = new byte[count];
ReadExactly(stream, buffer, 0, count);
return buffer;
}
/// <summary>
/// Reads exactly <paramref name="count"/> bytes from <paramref name="stream"/> into
/// <paramref name="buffer"/>, starting at the byte given by <paramref name="offset"/>.
/// </summary>
/// <param name="stream">The stream to read from.</param>
/// <param name="buffer">The buffer to read data into.</param>
/// <param name="offset">The offset within the buffer at which data is first written.</param>
/// <param name="count">The count of bytes to read.</param>
public static void ReadExactly(this Stream stream, byte[] buffer, int offset, int count)
{
// check arguments
if (stream == null)
throw new ArgumentNullException("stream");
if (buffer == null)
throw new ArgumentNullException("buffer");
if (offset < 0 || offset > buffer.Length)
throw new ArgumentOutOfRangeException("offset");
if (count < 0 || buffer.Length - offset < count)
throw new ArgumentOutOfRangeException("count");
while (count > 0)
{
// read data
int bytesRead = stream.Read(buffer, offset, count);
// check for failure to read
if (bytesRead == 0)
throw new EndOfStreamException();
// move to next block
offset += bytesRead;
count -= bytesRead;
}
}
}
}

45
src/ContentType.cs Normal file
View File

@@ -0,0 +1,45 @@
using System;
using System.Linq;
using System.Xml;
namespace Squirrel
{
internal static class ContentType
{
public static void Merge(XmlDocument doc)
{
var elements = new [] {
Tuple.Create("Default", "diff", "application/octet" ),
Tuple.Create("Default", "exe", "application/octet" ),
Tuple.Create("Default", "dll", "application/octet" ),
Tuple.Create("Default", "shasum", "text/plain" ),
};
var typesElement = doc.FirstChild.NextSibling;
if (typesElement.Name.ToLowerInvariant() != "types") {
throw new Exception("Invalid ContentTypes file, expected root node should be 'Types'");
}
var existingTypes = typesElement.ChildNodes.OfType<XmlElement>()
.Select(k => Tuple.Create(k.Name,
k.GetAttribute("Extension").ToLowerInvariant(),
k.GetAttribute("ContentType").ToLowerInvariant()));
var toAdd = elements
.Where(x => existingTypes.All(t => t.Item2 != x.Item2.ToLowerInvariant()))
.Select(element => {
var ret = doc.CreateElement(element.Item1, typesElement.NamespaceURI);
var ext = doc.CreateAttribute("Extension"); ext.Value = element.Item2;
var ct = doc.CreateAttribute("ContentType"); ct.Value = element.Item3;
ret.Attributes.Append(ext);
ret.Attributes.Append(ct);
return ret;
});
foreach (var v in toAdd) typesElement.AppendChild(v);
}
}
}

264
src/DeltaPackage.cs Normal file
View File

@@ -0,0 +1,264 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Ionic.Zip;
using Splat;
namespace Squirrel
{
public interface IDeltaPackageBuilder
{
ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile);
ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile);
}
public class DeltaPackageBuilder : IEnableLogger, IDeltaPackageBuilder
{
public ReleasePackage CreateDeltaPackage(ReleasePackage basePackage, ReleasePackage newPackage, string outputFile)
{
Contract.Requires(basePackage != null);
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
if (basePackage.Version > newPackage.Version) {
var message = String.Format(
"You cannot create a delta package based on version {0} as it is a later version than {1}",
basePackage.Version,
newPackage.Version);
throw new InvalidOperationException(message);
}
if (basePackage.ReleasePackageFile == null) {
throw new ArgumentException("The base package's release file is null", "basePackage");
}
if (!File.Exists(basePackage.ReleasePackageFile)) {
throw new FileNotFoundException("The base package release does not exist", basePackage.ReleasePackageFile);
}
if (!File.Exists(newPackage.ReleasePackageFile)) {
throw new FileNotFoundException("The new package release does not exist", newPackage.ReleasePackageFile);
}
string baseTempPath = null;
string tempPath = null;
using (Utility.WithTempDirectory(out baseTempPath))
using (Utility.WithTempDirectory(out tempPath)) {
var baseTempInfo = new DirectoryInfo(baseTempPath);
var tempInfo = new DirectoryInfo(tempPath);
using (var zf = new ZipFile(basePackage.ReleasePackageFile)) {
zf.ExtractAll(baseTempInfo.FullName);
}
using (var zf = new ZipFile(newPackage.ReleasePackageFile)) {
zf.ExtractAll(tempInfo.FullName);
}
// Collect a list of relative paths under 'lib' and map them
// to their full name. We'll use this later to determine in
// the new version of the package whether the file exists or
// not.
var baseLibFiles = baseTempInfo.GetAllFilesRecursively()
.Where(x => x.FullName.ToLowerInvariant().Contains("lib" + Path.DirectorySeparatorChar))
.ToDictionary(k => k.FullName.Replace(baseTempInfo.FullName, ""), v => v.FullName);
var newLibDir = tempInfo.GetDirectories().First(x => x.Name.ToLowerInvariant() == "lib");
foreach (var libFile in newLibDir.GetAllFilesRecursively()) {
createDeltaForSingleFile(libFile, tempInfo, baseLibFiles);
}
ReleasePackage.addDeltaFilesToContentTypes(tempInfo.FullName);
using (var zf = new ZipFile(outputFile)) {
zf.AddDirectory(tempInfo.FullName);
zf.Save();
}
}
return new ReleasePackage(outputFile);
}
public ReleasePackage ApplyDeltaPackage(ReleasePackage basePackage, ReleasePackage deltaPackage, string outputFile)
{
Contract.Requires(deltaPackage != null);
Contract.Requires(!String.IsNullOrEmpty(outputFile) && !File.Exists(outputFile));
string workingPath;
string deltaPath;
using (Utility.WithTempDirectory(out deltaPath))
using (Utility.WithTempDirectory(out workingPath))
using (var deltaZip = new ZipFile(deltaPackage.InputPackageFile))
using (var baseZip = new ZipFile(basePackage.InputPackageFile)) {
deltaZip.ExtractAll(deltaPath);
baseZip.ExtractAll(workingPath);
var pathsVisited = new List<string>();
var deltaPathRelativePaths = new DirectoryInfo(deltaPath).GetAllFilesRecursively()
.Select(x => x.FullName.Replace(deltaPath + Path.DirectorySeparatorChar, ""))
.ToArray();
// Apply all of the .diff files
deltaPathRelativePaths
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
.ForEach(file => {
pathsVisited.Add(Regex.Replace(file, @".diff$", "").ToLowerInvariant());
applyDiffToFile(deltaPath, file, workingPath);
});
// Delete all of the files that were in the old package but
// not in the new one.
new DirectoryInfo(workingPath).GetAllFilesRecursively()
.Select(x => x.FullName.Replace(workingPath + Path.DirectorySeparatorChar, "").ToLowerInvariant())
.Where(x => x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase) && !pathsVisited.Contains(x))
.ForEach(x => {
this.Log().Info("{0} was in old package but not in new one, deleting", x);
File.Delete(Path.Combine(workingPath, x));
});
// Update all the files that aren't in 'lib' with the delta
// package's versions (i.e. the nuspec file, etc etc).
deltaPathRelativePaths
.Where(x => !x.StartsWith("lib", StringComparison.InvariantCultureIgnoreCase))
.ForEach(x => {
this.Log().Info("Updating metadata file: {0}", x);
File.Copy(Path.Combine(deltaPath, x), Path.Combine(workingPath, x), true);
});
using (var zf = new ZipFile(outputFile)) {
zf.AddDirectory(workingPath);
zf.Save();
}
}
return new ReleasePackage(outputFile);
}
void createDeltaForSingleFile(FileInfo targetFile, DirectoryInfo workingDirectory, Dictionary<string, string> baseFileListing)
{
// NB: There are three cases here that we'll handle:
//
// 1. Exists only in new => leave it alone, we'll use it directly.
// 2. Exists in both old and new => write a dummy file so we know
// to keep it.
// 3. Exists in old but changed in new => create a delta file
//
// The fourth case of "Exists only in old => delete it in new"
// is handled when we apply the delta package
var relativePath = targetFile.FullName.Replace(workingDirectory.FullName, "");
if (!baseFileListing.ContainsKey(relativePath)) {
this.Log().Info("{0} not found in base package, marking as new", relativePath);
return;
}
var oldData = File.ReadAllBytes(baseFileListing[relativePath]);
var newData = File.ReadAllBytes(targetFile.FullName);
if (bytesAreIdentical(oldData, newData)) {
this.Log().Info("{0} hasn't changed, writing dummy file", relativePath);
File.Create(targetFile.FullName + ".diff").Dispose();
File.Create(targetFile.FullName + ".shasum").Dispose();
targetFile.Delete();
return;
}
this.Log().Info("Delta patching {0} => {1}", baseFileListing[relativePath], targetFile.FullName);
using (var of = File.Create(targetFile.FullName + ".diff")) {
BinaryPatchUtility.Create(oldData, newData, of);
var rl = ReleaseEntry.GenerateFromFile(new MemoryStream(newData), targetFile.Name + ".shasum");
File.WriteAllText(targetFile.FullName + ".shasum", rl.EntryAsString, Encoding.UTF8);
targetFile.Delete();
}
}
void applyDiffToFile(string deltaPath, string relativeFilePath, string workingDirectory)
{
var inputFile = Path.Combine(deltaPath, relativeFilePath);
var finalTarget = Path.Combine(workingDirectory, Regex.Replace(relativeFilePath, @".diff$", ""));
var tempTargetFile = Path.GetTempFileName();
// NB: Zero-length diffs indicate the file hasn't actually changed
if (new FileInfo(inputFile).Length == 0) {
this.Log().Info("{0} exists unchanged, skipping", relativeFilePath);
return;
}
if (relativeFilePath.EndsWith(".diff", StringComparison.InvariantCultureIgnoreCase)) {
using (var of = File.OpenWrite(tempTargetFile))
using (var inf = File.OpenRead(finalTarget)) {
this.Log().Info("Applying Diff to {0}", relativeFilePath);
BinaryPatchUtility.Apply(inf, () => File.OpenRead(inputFile), of);
}
try {
verifyPatchedFile(relativeFilePath, inputFile, tempTargetFile);
} catch (Exception) {
File.Delete(tempTargetFile);
throw;
}
} else {
using (var of = File.OpenWrite(tempTargetFile))
using (var inf = File.OpenRead(inputFile)) {
this.Log().Info("Adding new file: {0}", relativeFilePath);
inf.CopyTo(of);
}
}
if (File.Exists(finalTarget)) File.Delete(finalTarget);
var targetPath = Directory.GetParent(finalTarget);
if (!targetPath.Exists) targetPath.Create();
File.Move(tempTargetFile, finalTarget);
}
void verifyPatchedFile(string relativeFilePath, string inputFile, string tempTargetFile)
{
var shaFile = Regex.Replace(inputFile, @"\.diff$", ".shasum");
var expectedReleaseEntry = ReleaseEntry.ParseReleaseEntry(File.ReadAllText(shaFile, Encoding.UTF8));
var actualReleaseEntry = ReleaseEntry.GenerateFromFile(tempTargetFile);
if (expectedReleaseEntry.Filesize != actualReleaseEntry.Filesize) {
this.Log().Warn("Patched file {0} has incorrect size, expected {1}, got {2}", relativeFilePath,
expectedReleaseEntry.Filesize, actualReleaseEntry.Filesize);
throw new ChecksumFailedException() {Filename = relativeFilePath};
}
if (expectedReleaseEntry.SHA1 != actualReleaseEntry.SHA1) {
this.Log().Warn("Patched file {0} has incorrect SHA1, expected {1}, got {2}", relativeFilePath,
expectedReleaseEntry.SHA1, actualReleaseEntry.SHA1);
throw new ChecksumFailedException() {Filename = relativeFilePath};
}
}
bool bytesAreIdentical(byte[] oldData, byte[] newData)
{
if (oldData == null || newData == null) {
return oldData == newData;
}
if (oldData.LongLength != newData.LongLength) {
return false;
}
for(long i = 0; i < newData.LongLength; i++) {
if (oldData[i] != newData[i]) {
return false;
}
}
return true;
}
}
}

283
src/EnumerableExtensions.cs Normal file
View File

@@ -0,0 +1,283 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt
using System;
using System.Linq;
using System.Collections.Generic;
namespace Squirrel
{
internal static class EnumerableExtensions
{
/// <summary>
/// Enumerates the sequence and invokes the given action for each value in the sequence.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="onNext">Action to invoke for each element.</param>
public static void ForEach<TSource>(this IEnumerable<TSource> source, Action<TSource> onNext)
{
if (source == null)
throw new ArgumentNullException("source");
if (onNext == null)
throw new ArgumentNullException("onNext");
foreach (var item in source) onNext(item);
}
/// <summary>
/// Returns the elements with the maximum key value by using the default comparer to compare key values.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <typeparam name="TKey">Key type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="keySelector">Key selector used to extract the key for each element in the sequence.</param>
/// <returns>List with the elements that share the same maximum key value.</returns>
public static IList<TSource> MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
if (source == null)
throw new ArgumentNullException("source");
if (keySelector == null)
throw new ArgumentNullException("keySelector");
return MaxBy(source, keySelector, Comparer<TKey>.Default);
}
/// <summary>
/// Returns the elements with the minimum key value by using the specified comparer to compare key values.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <typeparam name="TKey">Key type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="keySelector">Key selector used to extract the key for each element in the sequence.</param>
/// <param name="comparer">Comparer used to determine the maximum key value.</param>
/// <returns>List with the elements that share the same maximum key value.</returns>
public static IList<TSource> MaxBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IComparer<TKey> comparer)
{
if (source == null)
throw new ArgumentNullException("source");
if (keySelector == null)
throw new ArgumentNullException("keySelector");
if (comparer == null)
throw new ArgumentNullException("comparer");
return ExtremaBy(source, keySelector, (key, minValue) => comparer.Compare(key, minValue));
}
private static IList<TSource> ExtremaBy<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TKey, TKey, int> compare)
{
var result = new List<TSource>();
using (var e = source.GetEnumerator())
{
if (!e.MoveNext())
throw new InvalidOperationException("Source sequence doesn't contain any elements.");
var current = e.Current;
var resKey = keySelector(current);
result.Add(current);
while (e.MoveNext())
{
var cur = e.Current;
var key = keySelector(cur);
var cmp = compare(key, resKey);
if (cmp == 0)
{
result.Add(cur);
}
else if (cmp > 0)
{
result = new List<TSource> { cur };
resKey = key;
}
}
}
return result;
}
/// <summary>
/// Lazily invokes an action for each value in the sequence.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="onNext">Action to invoke for each element.</param>
/// <returns>Sequence exhibiting the specified side-effects upon enumeration.</returns>
public static IEnumerable<TSource> Do<TSource>(this IEnumerable<TSource> source, Action<TSource> onNext)
{
if (source == null)
throw new ArgumentNullException("source");
if (onNext == null)
throw new ArgumentNullException("onNext");
return DoHelper(source, onNext, _ => { }, () => { });
}
/// <summary>
/// Lazily invokes an action for each value in the sequence, and executes an action for successful termination.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="onNext">Action to invoke for each element.</param>
/// <param name="onCompleted">Action to invoke on successful termination of the sequence.</param>
/// <returns>Sequence exhibiting the specified side-effects upon enumeration.</returns>
public static IEnumerable<TSource> Do<TSource>(this IEnumerable<TSource> source, Action<TSource> onNext, Action onCompleted)
{
if (source == null)
throw new ArgumentNullException("source");
if (onNext == null)
throw new ArgumentNullException("onNext");
if (onCompleted == null)
throw new ArgumentNullException("onCompleted");
return DoHelper(source, onNext, _ => { }, onCompleted);
}
/// <summary>
/// Lazily invokes an action for each value in the sequence, and executes an action upon exceptional termination.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="onNext">Action to invoke for each element.</param>
/// <param name="onError">Action to invoke on exceptional termination of the sequence.</param>
/// <returns>Sequence exhibiting the specified side-effects upon enumeration.</returns>
public static IEnumerable<TSource> Do<TSource>(this IEnumerable<TSource> source, Action<TSource> onNext, Action<Exception> onError)
{
if (source == null)
throw new ArgumentNullException("source");
if (onNext == null)
throw new ArgumentNullException("onNext");
if (onError == null)
throw new ArgumentNullException("onError");
return DoHelper(source, onNext, onError, () => { });
}
/// <summary>
/// Lazily invokes an action for each value in the sequence, and executes an action upon successful or exceptional termination.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="onNext">Action to invoke for each element.</param>
/// <param name="onError">Action to invoke on exceptional termination of the sequence.</param>
/// <param name="onCompleted">Action to invoke on successful termination of the sequence.</param>
/// <returns>Sequence exhibiting the specified side-effects upon enumeration.</returns>
public static IEnumerable<TSource> Do<TSource>(this IEnumerable<TSource> source, Action<TSource> onNext, Action<Exception> onError, Action onCompleted)
{
if (source == null)
throw new ArgumentNullException("source");
if (onNext == null)
throw new ArgumentNullException("onNext");
if (onError == null)
throw new ArgumentNullException("onError");
if (onCompleted == null)
throw new ArgumentNullException("onCompleted");
return DoHelper(source, onNext, onError, onCompleted);
}
private static IEnumerable<TSource> DoHelper<TSource>(this IEnumerable<TSource> source, Action<TSource> onNext, Action<Exception> onError, Action onCompleted)
{
using (var e = source.GetEnumerator())
{
while (true)
{
var current = default(TSource);
try
{
if (!e.MoveNext())
break;
current = e.Current;
}
catch (Exception ex)
{
onError(ex);
throw;
}
onNext(current);
yield return current;
}
onCompleted();
}
}
/// <summary>
/// Returns the source sequence prefixed with the specified value.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="values">Values to prefix the sequence with.</param>
/// <returns>Sequence starting with the specified prefix value, followed by the source sequence.</returns>
public static IEnumerable<TSource> StartWith<TSource>(this IEnumerable<TSource> source, params TSource[] values)
{
if (source == null)
throw new ArgumentNullException("source");
return source.StartWith_(values);
}
static IEnumerable<TSource> StartWith_<TSource>(this IEnumerable<TSource> source, params TSource[] values)
{
foreach (var x in values)
yield return x;
foreach (var item in source)
yield return item;
}
/// <summary>
/// Returns elements with a distinct key value by using the default equality comparer to compare key values.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <typeparam name="TKey">Key type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="keySelector">Key selector.</param>
/// <returns>Sequence that contains the elements from the source sequence with distinct key values.</returns>
public static IEnumerable<TSource> Distinct<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
{
if (source == null)
throw new ArgumentNullException("source");
if (keySelector == null)
throw new ArgumentNullException("keySelector");
return source.Distinct_(keySelector, EqualityComparer<TKey>.Default);
}
/// <summary>
/// Returns elements with a distinct key value by using the specified equality comparer to compare key values.
/// </summary>
/// <typeparam name="TSource">Source sequence element type.</typeparam>
/// <typeparam name="TKey">Key type.</typeparam>
/// <param name="source">Source sequence.</param>
/// <param name="keySelector">Key selector.</param>
/// <param name="comparer">Comparer used to compare key values.</param>
/// <returns>Sequence that contains the elements from the source sequence with distinct key values.</returns>
public static IEnumerable<TSource> Distinct<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
{
if (source == null)
throw new ArgumentNullException("source");
if (keySelector == null)
throw new ArgumentNullException("keySelector");
if (comparer == null)
throw new ArgumentNullException("comparer");
return source.Distinct_(keySelector, comparer);
}
static IEnumerable<TSource> Distinct_<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector, IEqualityComparer<TKey> comparer)
{
var set = new HashSet<TKey>(comparer);
foreach (var item in source)
{
var key = keySelector(item);
if (set.Add(key))
yield return item;
}
}
}
}

1779
src/MarkdownSharp.cs Normal file

File diff suppressed because it is too large Load Diff

17
src/PackageExtensions.cs Normal file
View File

@@ -0,0 +1,17 @@
using System;
using NuGet;
namespace Squirrel
{
public static class PackageExtensions
{
public static string ExtractTitle(this IPackage package)
{
if (package == null)
return String.Empty;
var title = package.Title;
return !String.IsNullOrWhiteSpace(title) ? title : package.Id;
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Squirrel")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Squirrel")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("3c25a7f9-3e99-4556-aba3-f820c74bb4da")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: InternalsVisibleTo("Squirrel.Tests")]

199
src/ReleaseEntry.cs Normal file
View File

@@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using NuGet;
using Splat;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace Squirrel
{
public interface IReleaseEntry
{
string SHA1 { get; }
string Filename { get; }
long Filesize { get; }
bool IsDelta { get; }
string EntryAsString { get; }
Version Version { get; }
string PackageName { get; }
string GetReleaseNotes(string packageDirectory);
}
[DataContract]
public class ReleaseEntry : IEnableLogger, IReleaseEntry
{
[DataMember] public string SHA1 { get; protected set; }
[DataMember] public string Filename { get; protected set; }
[DataMember] public long Filesize { get; protected set; }
[DataMember] public bool IsDelta { get; protected set; }
protected ReleaseEntry(string sha1, string filename, long filesize, bool isDelta)
{
Contract.Requires(sha1 != null && sha1.Length == 40);
Contract.Requires(filename != null);
Contract.Requires(filename.Contains(Path.DirectorySeparatorChar) == false);
Contract.Requires(filesize > 0);
SHA1 = sha1; Filename = filename; Filesize = filesize; IsDelta = isDelta;
}
[IgnoreDataMember]
public string EntryAsString {
get { return String.Format("{0} {1} {2}", SHA1, Filename, Filesize); }
}
[IgnoreDataMember]
public Version Version { get { return Filename.ToVersion(); } }
[IgnoreDataMember]
public string PackageName {
get {
return Filename.Substring(0, Filename.IndexOfAny(new[] { '-', '.' }));
}
}
public string GetReleaseNotes(string packageDirectory)
{
var zp = new ZipPackage(Path.Combine(packageDirectory, Filename));
var t = zp.Id;
if (String.IsNullOrWhiteSpace(zp.ReleaseNotes))
throw new Exception(String.Format("Invalid 'ReleaseNotes' value in nuspec file at '{0}'", Path.Combine(packageDirectory, Filename)));
return zp.ReleaseNotes;
}
static readonly Regex entryRegex = new Regex(@"^([0-9a-fA-F]{40})\s+(\S+)\s+(\d+)[\r]*$");
static readonly Regex commentRegex = new Regex(@"#.*$");
public static ReleaseEntry ParseReleaseEntry(string entry)
{
Contract.Requires(entry != null);
entry = commentRegex.Replace(entry, "");
if (String.IsNullOrWhiteSpace(entry)) {
return null;
}
var m = entryRegex.Match(entry);
if (!m.Success) {
throw new Exception("Invalid release entry: " + entry);
}
if (m.Groups.Count != 4) {
throw new Exception("Invalid release entry: " + entry);
}
long size = Int64.Parse(m.Groups[3].Value);
bool isDelta = filenameIsDeltaFile(m.Groups[2].Value);
return new ReleaseEntry(m.Groups[1].Value, m.Groups[2].Value, size, isDelta);
}
public static IEnumerable<ReleaseEntry> ParseReleaseFile(string fileContents)
{
if (String.IsNullOrEmpty(fileContents)) {
return new ReleaseEntry[0];
}
var ret = fileContents.Split('\n')
.Where(x => !String.IsNullOrWhiteSpace(x))
.Select(ParseReleaseEntry)
.Where(x => x != null)
.ToArray();
return ret.Any(x => x == null) ? null : ret;
}
public static void WriteReleaseFile(IEnumerable<ReleaseEntry> releaseEntries, Stream stream)
{
Contract.Requires(releaseEntries != null && releaseEntries.Any());
Contract.Requires(stream != null);
using (var sw = new StreamWriter(stream, Encoding.UTF8)) {
sw.Write(String.Join("\n", releaseEntries
.OrderBy(x => x.Version)
.ThenByDescending(x => x.IsDelta)
.Select(x => x.EntryAsString)));
}
}
public static void WriteReleaseFile(IEnumerable<ReleaseEntry> releaseEntries, string path)
{
Contract.Requires(releaseEntries != null && releaseEntries.Any());
Contract.Requires(!String.IsNullOrEmpty(path));
using (var f = File.OpenWrite(path)) {
WriteReleaseFile(releaseEntries, f);
}
}
public static ReleaseEntry GenerateFromFile(Stream file, string filename)
{
Contract.Requires(file != null && file.CanRead);
Contract.Requires(!String.IsNullOrEmpty(filename));
var hash = Utility.CalculateStreamSHA1(file);
return new ReleaseEntry(hash, filename, file.Length, filenameIsDeltaFile(filename));
}
public static ReleaseEntry GenerateFromFile(string path)
{
using (var inf = File.OpenRead(path)) {
return GenerateFromFile(inf, Path.GetFileName(path));
}
}
public static void BuildReleasesFile(string releasePackagesDir)
{
var packagesDir = new DirectoryInfo(releasePackagesDir);
// Generate release entries for all of the local packages
var entriesQueue = new ConcurrentQueue<ReleaseEntry>();
Parallel.ForEach(packagesDir.GetFiles("*.nupkg"), x => {
using (var file = x.OpenRead()) {
entriesQueue.Enqueue(GenerateFromFile(file, x.Name));
}
});
// Write the new RELEASES file to a temp file then move it into
// place
var entries = entriesQueue.ToList();
var tempFile = Path.GetTempFileName();
using (var of = File.OpenWrite(tempFile)) {
if (entries.Count > 0) WriteReleaseFile(entries, of);
}
var target = Path.Combine(packagesDir.FullName, "RELEASES");
if (File.Exists(target)) {
File.Delete(target);
}
File.Move(tempFile, target);
}
static bool filenameIsDeltaFile(string filename)
{
return filename.EndsWith("-delta.nupkg", StringComparison.InvariantCultureIgnoreCase);
}
public static ReleasePackage GetPreviousRelease(IEnumerable<ReleaseEntry> releaseEntries, IReleasePackage package, string targetDir)
{
if (releaseEntries == null || !releaseEntries.Any())
return null;
return releaseEntries
.Where(x => x.IsDelta == false)
.Where(x => x.Version < package.ToVersion())
.OrderByDescending(x => x.Version)
.Select(x => new ReleasePackage(Path.Combine(targetDir, x.Filename), true))
.FirstOrDefault();
}
}
}

49
src/ReleaseExtensions.cs Normal file
View File

@@ -0,0 +1,49 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
namespace Squirrel
{
public static class VersionExtensions
{
public static Version ToVersion(this IReleasePackage package)
{
return package.InputPackageFile.ToVersion();
}
public static Version ToVersion(this string fileName)
{
var parts = (new FileInfo(fileName)).Name
.Replace(".nupkg", "").Replace("-delta", "")
.Split('.', '-').Reverse();
var numberRegex = new Regex(@"^\d+$");
var versionFields = parts
.Where(x => numberRegex.IsMatch(x))
.Select(Int32.Parse)
.Reverse()
.ToArray();
if (versionFields.Length < 2 || versionFields.Length > 4)
{
return null;
}
switch (versionFields.Length)
{
case 2:
return new Version(versionFields[0], versionFields[1]);
case 3:
return new Version(versionFields[0], versionFields[1], versionFields[2]);
case 4:
return new Version(versionFields[0], versionFields[1], versionFields[2], versionFields[3]);
}
return null;
}
}
}

280
src/ReleasePackage.cs Normal file
View File

@@ -0,0 +1,280 @@
using System;
using System.Collections.Generic;
using System.ComponentModel.Design;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml;
using Ionic.Zip;
using NuGet;
using Splat;
namespace Squirrel
{
internal static class FrameworkTargetVersion
{
public static FrameworkName Net40 = new FrameworkName(".NETFramework,Version=v4.0");
public static FrameworkName Net45 = new FrameworkName(".NETFramework,Version=v4.5");
}
public interface IReleasePackage
{
string InputPackageFile { get; }
string ReleasePackageFile { get; }
string SuggestedReleaseFileName { get; }
string CreateReleasePackage(string outputFile, string packagesRootDir = null, Func<string, string> releaseNotesProcessor = null);
}
public static class VersionComparer
{
public static bool Matches(IVersionSpec versionSpec, SemanticVersion version)
{
if (versionSpec == null)
return true; // I CAN'T DEAL WITH THIS
bool minVersion;
if (versionSpec.MinVersion == null) {
minVersion = true; // no preconditon? LET'S DO IT
} else if (versionSpec.IsMinInclusive) {
minVersion = version >= versionSpec.MinVersion;
} else {
minVersion = version > versionSpec.MinVersion;
}
bool maxVersion;
if (versionSpec.MaxVersion == null) {
maxVersion = true; // no preconditon? LET'S DO IT
} else if (versionSpec.IsMaxInclusive) {
maxVersion = version <= versionSpec.MaxVersion;
} else {
maxVersion = version < versionSpec.MaxVersion;
}
return maxVersion && minVersion;
}
}
public class ReleasePackage : IEnableLogger, IReleasePackage
{
IEnumerable<IPackage> localPackageCache;
public ReleasePackage(string inputPackageFile, bool isReleasePackage = false)
{
InputPackageFile = inputPackageFile;
if (isReleasePackage) {
ReleasePackageFile = inputPackageFile;
}
}
public string InputPackageFile { get; protected set; }
public string ReleasePackageFile { get; protected set; }
public string SuggestedReleaseFileName {
get {
var zp = new ZipPackage(InputPackageFile);
return String.Format("{0}-{1}-full.nupkg", zp.Id, zp.Version);
}
}
public Version Version { get { return InputPackageFile.ToVersion(); } }
public string CreateReleasePackage(string outputFile, string packagesRootDir = null, Func<string, string> releaseNotesProcessor = null)
{
Contract.Requires(!String.IsNullOrEmpty(outputFile));
if (ReleasePackageFile != null) {
return ReleasePackageFile;
}
var package = new ZipPackage(InputPackageFile);
// we can tell from here what platform(s) the package targets
// but given this is a simple package we only
// ever expect one entry here (crash hard otherwise)
var frameworks = package.GetSupportedFrameworks();
if (frameworks.Count() > 1) {
var platforms = frameworks
.Aggregate(new StringBuilder(), (sb, f) => sb.Append(f.ToString() + "; "));
throw new InvalidOperationException(String.Format(
"The input package file {0} targets multiple platforms - {1} - and cannot be transformed into a release package.", InputPackageFile, platforms));
}
var targetFramework = frameworks.Single();
// Recursively walk the dependency tree and extract all of the
// dependent packages into the a temporary directory
var dependencies = findAllDependentPackages(
package,
new LocalPackageRepository(packagesRootDir),
frameworkName: targetFramework);
string tempPath = null;
using (Utility.WithTempDirectory(out tempPath)) {
var tempDir = new DirectoryInfo(tempPath);
using(var zf = new ZipFile(InputPackageFile)) {
zf.ExtractAll(tempPath);
}
extractDependentPackages(dependencies, tempDir, targetFramework);
var specPath = tempDir.GetFiles("*.nuspec").First().FullName;
removeDependenciesFromPackageSpec(specPath);
removeDeveloperDocumentation(tempDir);
if (releaseNotesProcessor != null) {
renderReleaseNotesMarkdown(specPath, releaseNotesProcessor);
}
addDeltaFilesToContentTypes(tempDir.FullName);
using (var zf = new ZipFile(outputFile)) {
zf.AddDirectory(tempPath);
zf.Save();
}
ReleasePackageFile = outputFile;
return ReleasePackageFile;
}
}
void extractDependentPackages(IEnumerable<IPackage> dependencies, DirectoryInfo tempPath, FrameworkName framework)
{
dependencies.ForEach(pkg => {
this.Log().Info("Scanning {0}", pkg.Id);
pkg.GetLibFiles().ForEach(file => {
var outPath = new FileInfo(Path.Combine(tempPath.FullName, file.Path));
if (!VersionUtility.IsCompatible(framework , new[] { file.TargetFramework }))
{
this.Log().Info("Ignoring {0} as the target framework is not compatible", outPath);
return;
}
Directory.CreateDirectory(outPath.Directory.FullName);
using (var of = File.Create(outPath.FullName)) {
this.Log().Info("Writing {0} to {1}", file.Path, outPath);
file.GetStream().CopyTo(of);
}
});
});
}
void removeDeveloperDocumentation(DirectoryInfo expandedRepoPath)
{
expandedRepoPath.GetAllFilesRecursively()
.Where(x => x.Name.EndsWith(".dll", true, CultureInfo.InvariantCulture))
.Select(x => new FileInfo(x.FullName.ToLowerInvariant().Replace(".dll", ".xml")))
.Where(x => x.Exists)
.ForEach(x => x.Delete());
}
void renderReleaseNotesMarkdown(string specPath, Func<string, string> releaseNotesProcessor)
{
var doc = new XmlDocument();
doc.Load(specPath);
// XXX: This code looks full tart
var metadata = doc.DocumentElement.ChildNodes
.OfType<XmlElement>()
.First(x => x.Name.ToLowerInvariant() == "metadata");
var releaseNotes = metadata.ChildNodes
.OfType<XmlElement>()
.FirstOrDefault(x => x.Name.ToLowerInvariant() == "releasenotes");
if (releaseNotes == null) {
this.Log().Info("No release notes found in {0}", specPath);
return;
}
releaseNotes.InnerText = String.Format("<![CDATA[\n" + "{0}\n" + "]]>",
releaseNotesProcessor(releaseNotes.InnerText));
doc.Save(specPath);
}
void removeDependenciesFromPackageSpec(string specPath)
{
var xdoc = new XmlDocument();
xdoc.Load(specPath);
var metadata = xdoc.DocumentElement.FirstChild;
var dependenciesNode = metadata.ChildNodes.OfType<XmlElement>().FirstOrDefault(x => x.Name.ToLowerInvariant() == "dependencies");
if (dependenciesNode != null) {
metadata.RemoveChild(dependenciesNode);
}
xdoc.Save(specPath);
}
internal IEnumerable<IPackage> findAllDependentPackages(
IPackage package = null,
IPackageRepository packageRepository = null,
HashSet<string> packageCache = null,
FrameworkName frameworkName = null)
{
package = package ?? new ZipPackage(InputPackageFile);
packageCache = packageCache ?? new HashSet<string>();
var deps = package.DependencySets
.Where(x => x.TargetFramework == null
|| x.TargetFramework == frameworkName)
.SelectMany(x => x.Dependencies);
return deps.SelectMany(dependency => {
var ret = matchPackage(packageRepository, dependency.Id, dependency.VersionSpec);
if (ret == null) {
var message = String.Format("Couldn't find file for package in {1}: {0}", dependency.Id, packageRepository.Source);
this.Log().Error(message);
throw new Exception(message);
}
if (packageCache.Contains(ret.GetFullName())) {
return Enumerable.Empty<IPackage>();
}
packageCache.Add(ret.GetFullName());
return findAllDependentPackages(ret, packageRepository, packageCache, frameworkName).StartWith(ret).Distinct(y => y.GetFullName());
}).ToArray();
}
IPackage matchPackage(IPackageRepository packageRepository, string id, IVersionSpec version)
{
return packageRepository.FindPackagesById(id).FirstOrDefault(x => VersionComparer.Matches(version, x.Version));
}
static internal void addDeltaFilesToContentTypes(string rootDirectory)
{
var doc = new XmlDocument();
var path = Path.Combine(rootDirectory, "[Content_Types].xml");
doc.Load(path);
ContentType.Merge(doc);
using (var sw = new StreamWriter(path, false, Encoding.UTF8)) {
doc.Save(sw);
}
}
}
public class ChecksumFailedException : Exception
{
public string Filename { get; set; }
}
}

79
src/Squirrel.csproj Normal file
View File

@@ -0,0 +1,79 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{1436E22A-FE3C-4D68-9A85-9E74DF2E6A92}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Squirrel</RootNamespace>
<AssemblyName>Squirrel</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Ionic.Zip">
<HintPath>..\ext\Ionic.Zip.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Web.XmlTransform">
<HintPath>..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll</HintPath>
</Reference>
<Reference Include="NuGet.Core, Version=2.8.50506.491, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\NuGet.Core.2.8.2\lib\net40-Client\NuGet.Core.dll</HintPath>
</Reference>
<Reference Include="Splat">
<HintPath>..\packages\Splat.1.4.0\lib\Net45\Splat.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Runtime.Serialization" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
</ItemGroup>
<ItemGroup>
<Compile Include="BinaryPatchUtility.cs" />
<Compile Include="ContentType.cs" />
<Compile Include="DeltaPackage.cs" />
<Compile Include="EnumerableExtensions.cs" />
<Compile Include="MarkdownSharp.cs" />
<Compile Include="PackageExtensions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReleaseEntry.cs" />
<Compile Include="ReleaseExtensions.cs" />
<Compile Include="ReleasePackage.cs" />
<Compile Include="Utility.cs" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

354
src/Utility.cs Normal file
View File

@@ -0,0 +1,354 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Cryptography;
using System.Security.Principal;
using System.Threading;
using Splat;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Concurrent;
namespace Squirrel
{
public static class Utility
{
public static IEnumerable<FileInfo> GetAllFilesRecursively(this DirectoryInfo rootPath)
{
Contract.Requires(rootPath != null);
return rootPath.GetDirectories()
.SelectMany(GetAllFilesRecursively)
.Concat(rootPath.GetFiles());
}
public static IEnumerable<string> GetAllFilePathsRecursively(string rootPath)
{
Contract.Requires(rootPath != null);
return Directory.GetDirectories(rootPath)
.SelectMany(GetAllFilePathsRecursively)
.Concat(Directory.GetFiles(rootPath));
}
public static string CalculateFileSHA1(string filePath)
{
Contract.Requires(filePath != null);
using (var stream = File.OpenRead(filePath)) {
return CalculateStreamSHA1(stream);
}
}
public static string CalculateStreamSHA1(Stream file)
{
Contract.Requires(file != null && file.CanRead);
using (var sha1 = SHA1.Create()) {
return BitConverter.ToString(sha1.ComputeHash(file)).Replace("-", String.Empty);
}
}
public static async Task CopyToAsync(string from, string to)
{
Contract.Requires(!String.IsNullOrEmpty(from) && File.Exists(from));
Contract.Requires(!String.IsNullOrEmpty(to));
if (!File.Exists(from)) {
Log().Warn("The file {0} does not exist", from);
// TODO: should we fail this operation?
return;
}
// XXX: SafeCopy
await Task.Run(() => File.Copy(from, to, true));
}
public static void Retry(this Action block, int retries = 2)
{
Contract.Requires(retries > 0);
Func<object> thunk = () => {
block();
return null;
};
thunk.Retry(retries);
}
public static T Retry<T>(this Func<T> block, int retries = 2)
{
Contract.Requires(retries > 0);
while (true) {
try {
T ret = block();
return ret;
} catch (Exception) {
if (retries == 0) {
throw;
}
retries--;
Thread.Sleep(250);
}
}
}
public static Task ForEachAsync<T>(this IEnumerable<T> source, Action<T> body, int degreeOfParallelism = 4)
{
return ForEachAsync(source, x => Task.Run(() => body(x)), degreeOfParallelism);
}
public static Task ForEachAsync<T>(this IEnumerable<T> source, Func<T, Task> body, int degreeOfParallelism = 4)
{
return Task.WhenAll(
from partition in Partitioner.Create(source).GetPartitions(degreeOfParallelism)
select Task.Run(async () => {
using (partition)
while (partition.MoveNext())
await body(partition.Current);
}));
}
static string directoryChars;
public static IDisposable WithTempDirectory(out string path)
{
var di = new DirectoryInfo(Environment.GetEnvironmentVariable("SQUIRREL_TEMP") ?? Environment.GetEnvironmentVariable("TEMP") ?? "");
if (!di.Exists) {
throw new Exception("%TEMP% isn't defined, go set it");
}
var tempDir = default(DirectoryInfo);
directoryChars = directoryChars ?? (
"abcdefghijklmnopqrstuvwxyz" +
Enumerable.Range(0x4E00, 0x9FCC - 0x4E00) // CJK UNIFIED IDEOGRAPHS
.Aggregate(new StringBuilder(), (acc, x) => { acc.Append(Char.ConvertFromUtf32(x)); return acc; })
.ToString());
foreach (var c in directoryChars) {
var target = Path.Combine(di.FullName, c.ToString());
if (!File.Exists(target) && !Directory.Exists(target)) {
Directory.CreateDirectory(target);
tempDir = new DirectoryInfo(target);
break;
}
}
path = tempDir.FullName;
return Disposable.Create(() =>
DeleteDirectory(tempDir.FullName).Wait());
}
public static async Task DeleteDirectory(string directoryPath)
{
Contract.Requires(!String.IsNullOrEmpty(directoryPath));
Log().Info("Starting to delete folder: {0}", directoryPath);
if (!Directory.Exists(directoryPath)) {
Log().Warn("DeleteDirectory: does not exist - {0}", directoryPath);
return;
}
// From http://stackoverflow.com/questions/329355/cannot-delete-directory-with-directory-deletepath-true/329502#329502
var files = new string[0];
try {
files = Directory.GetFiles(directoryPath);
} catch (UnauthorizedAccessException ex) {
var message = String.Format("The files inside {0} could not be read", directoryPath);
Log().Warn(message, ex);
}
var dirs = new string[0];
try {
dirs = Directory.GetDirectories(directoryPath);
} catch (UnauthorizedAccessException ex) {
var message = String.Format("The directories inside {0} could not be read", directoryPath);
Log().Warn(message, ex);
}
var fileOperations = files.ForEachAsync(file => {
File.SetAttributes(file, FileAttributes.Normal);
File.Delete(file);
});
var directoryOperations =
dirs.ForEachAsync(async dir => await DeleteDirectory(dir));
await Task.WhenAll(fileOperations, directoryOperations);
Log().Debug("Now deleting folder: {0}", directoryPath);
File.SetAttributes(directoryPath, FileAttributes.Normal);
try {
Directory.Delete(directoryPath, false);
} catch (Exception ex) {
var message = String.Format("DeleteDirectory: could not delete - {0}", directoryPath);
Log().ErrorException(message, ex);
}
}
public static Tuple<string, Stream> CreateTempFile()
{
var path = Path.GetTempFileName();
return Tuple.Create(path, (Stream) File.OpenWrite(path));
}
static TAcc scan<T, TAcc>(this IEnumerable<T> This, TAcc initialValue, Func<TAcc, T, TAcc> accFunc)
{
TAcc acc = initialValue;
foreach (var x in This)
{
acc = accFunc(acc, x);
}
return acc;
}
public static void DeleteDirectoryAtNextReboot(string directoryPath)
{
var di = new DirectoryInfo(directoryPath);
if (!di.Exists) {
Log().Warn("DeleteDirectoryAtNextReboot: does not exist - {0}", directoryPath);
return;
}
// NB: MoveFileEx blows up if you're a non-admin, so you always need a backup plan
di.GetFiles().ForEach(x => safeDeleteFileAtNextReboot(x.FullName));
di.GetDirectories().ForEach(x => DeleteDirectoryAtNextReboot(x.FullName));
safeDeleteFileAtNextReboot(directoryPath);
}
static void safeDeleteFileAtNextReboot(string name)
{
if (MoveFileEx(name, null, MoveFileFlags.MOVEFILE_DELAY_UNTIL_REBOOT)) return;
// thank you, http://www.pinvoke.net/default.aspx/coredll.getlasterror
var lastError = Marshal.GetLastWin32Error();
Log().Error("safeDeleteFileAtNextReboot: failed - {0} - {1}", name, lastError);
}
static IFullLogger logger;
static IFullLogger Log()
{
return logger ??
(logger = Locator.CurrentMutable.GetService<ILogManager>().GetLogger(typeof(Utility)));
}
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, MoveFileFlags dwFlags);
[Flags]
enum MoveFileFlags
{
MOVEFILE_REPLACE_EXISTING = 0x00000001,
MOVEFILE_COPY_ALLOWED = 0x00000002,
MOVEFILE_DELAY_UNTIL_REBOOT = 0x00000004,
MOVEFILE_WRITE_THROUGH = 0x00000008,
MOVEFILE_CREATE_HARDLINK = 0x00000010,
MOVEFILE_FAIL_IF_NOT_TRACKABLE = 0x00000020
}
}
/*
public sealed class SingleGlobalInstance : IDisposable
{
readonly static object gate = 42;
bool HasHandle = false;
Mutex mutex;
EventLoopScheduler lockScheduler = new EventLoopScheduler();
public SingleGlobalInstance(string key, int timeOut)
{
if (RxApp.InUnitTestRunner()) {
HasHandle = Observable.Start(() => Monitor.TryEnter(gate, timeOut), lockScheduler).First();
if (HasHandle == false)
throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance");
return;
}
initMutex(key);
try
{
if (timeOut <= 0)
HasHandle = Observable.Start(() => mutex.WaitOne(Timeout.Infinite, false), lockScheduler).First();
else
HasHandle = Observable.Start(() => mutex.WaitOne(timeOut, false), lockScheduler).First();
if (HasHandle == false)
throw new TimeoutException("Timeout waiting for exclusive access on SingleInstance");
}
catch (AbandonedMutexException)
{
HasHandle = true;
}
}
private void initMutex(string key)
{
string mutexId = string.Format("Global\\{{{0}}}", key);
mutex = new Mutex(false, mutexId);
var allowEveryoneRule = new MutexAccessRule(new SecurityIdentifier(WellKnownSidType.WorldSid, null), MutexRights.FullControl, AccessControlType.Allow);
var securitySettings = new MutexSecurity();
securitySettings.AddAccessRule(allowEveryoneRule);
mutex.SetAccessControl(securitySettings);
}
public void Dispose()
{
if (HasHandle && RxApp.InUnitTestRunner()) {
Observable.Start(() => Monitor.Exit(gate), lockScheduler).First();
HasHandle = false;
}
if (HasHandle && mutex != null) {
Observable.Start(() => mutex.ReleaseMutex(), lockScheduler).First();
HasHandle = false;
}
lockScheduler.Dispose();
}
}
*/
public static class Disposable
{
public static IDisposable Create(Action action)
{
return new AnonDisposable(action);
}
class AnonDisposable : IDisposable
{
static readonly Action dummyBlock = (() => { });
Action block;
public AnonDisposable(Action b)
{
block = b;
}
public void Dispose()
{
Interlocked.Exchange(ref block, dummyBlock)();
}
}
}
}

6
src/packages.config Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Web.Xdt" version="2.1.1" targetFramework="net45" />
<package id="NuGet.Core" version="2.8.2" targetFramework="net45" />
<package id="Splat" version="1.4.0" targetFramework="net45" />
</packages>

54
test/ContentTypeTests.cs Normal file
View File

@@ -0,0 +1,54 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Xml;
using Squirrel;
using Squirrel.Tests.TestHelpers;
using Xunit;
namespace Squirrel.Tests.Core
{
public class ContentTypeTests
{
[Theory]
[InlineData("basic.xml", "basic-merged.xml")]
[InlineData("complex.xml", "complex-merged.xml")]
public void MergeContentTypes(string inputFileName, string expectedFileName)
{
var inputFile = IntegrationTestHelper.GetPath("fixtures", "content-types", inputFileName);
var expectedFile = IntegrationTestHelper.GetPath("fixtures", "content-types", expectedFileName);
var tempFile = Path.GetTempFileName() + ".xml";
var expected = new XmlDocument();
expected.Load(expectedFile);
var existingTypes = GetContentTypes(expected);
try
{
File.Copy(inputFile, tempFile);
var actual = new XmlDocument();
actual.Load(tempFile);
ContentType.Merge(actual);
var actualTypes = GetContentTypes(actual);
Assert.Equal(existingTypes, actualTypes);
}
finally
{
File.Delete(tempFile);
}
}
static IEnumerable<XmlElement> GetContentTypes(XmlNode doc)
{
var expectedTypesElement = doc.FirstChild.NextSibling;
return expectedTypesElement.ChildNodes.OfType<XmlElement>();
}
}
}

336
test/DeltaPackageTests.cs Normal file
View File

@@ -0,0 +1,336 @@
using System;
using System.IO;
using System.Linq;
using NuGet;
using Splat;
using Squirrel;
using Squirrel.Tests.TestHelpers;
using Xunit;
namespace Squirrel.Tests.Core
{
public class ApplyDeltaPackageTests : IEnableLogger
{
[Fact]
public void ApplyDeltaPackageSmokeTest()
{
var basePackage = new ReleasePackage(IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.0.0.0-full.nupkg"));
var deltaPackage = new ReleasePackage(IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.1.0.0-delta.nupkg"));
var expectedPackageFile = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.1.0.0-full.nupkg");
var outFile = Path.GetTempFileName() + ".nupkg";
try {
var deltaBuilder = new DeltaPackageBuilder();
deltaBuilder.ApplyDeltaPackage(basePackage, deltaPackage, outFile);
var result = new ZipPackage(outFile);
var expected = new ZipPackage(expectedPackageFile);
result.Id.ShouldEqual(expected.Id);
result.Version.ShouldEqual(expected.Version);
this.Log().Info("Expected file list:");
expected.GetFiles().Select(x => x.Path).OrderBy(x => x).ForEach(x => this.Log().Info(x));
this.Log().Info("Actual file list:");
result.GetFiles().Select(x => x.Path).OrderBy(x => x).ForEach(x => this.Log().Info(x));
Enumerable.Zip(
expected.GetFiles().Select(x => x.Path).OrderBy(x => x),
result.GetFiles().Select(x => x.Path).OrderBy(x => x),
(e, a) => e == a
).All(x => x).ShouldBeTrue();
} finally {
if (File.Exists(outFile)) {
File.Delete(outFile);
}
}
}
[Fact]
public void ApplyMultipleDeltaPackagesGeneratesCorrectHash()
{
Assert.False(true, "UpdateManager not ready yet");
/*
var firstRelease = new ReleasePackage(IntegrationTestHelper.GetPath("fixtures", "SquirrelDesktopDemo-1.0.0-full.nupkg"), true);
var secondRelease = new ReleasePackage(IntegrationTestHelper.GetPath("fixtures", "SquirrelDesktopDemo-1.1.0-full.nupkg"), true);
var thirdRelease = new ReleasePackage(IntegrationTestHelper.GetPath("fixtures", "SquirrelDesktopDemo-1.2.0-full.nupkg"), true);
string installDir, releasesDir;
using(Utility.WithTempDirectory(out releasesDir))
using (IntegrationTestHelper.WithFakeAlreadyInstalledApp("InstalledSquirrelDesktopDemo-1.0.0.zip", out installDir)) {
var firstDelta = Path.Combine(releasesDir, "SquirrelDesktopDemo-1.1.0-delta.nupkg");
var secondDelta = Path.Combine(releasesDir, "SquirrelDesktopDemo-1.2.0-delta.nupkg");
new[] { firstRelease, secondRelease, thirdRelease }
.ForEach(file =>
{
var packageFile = file.ReleasePackageFile;
var fileName = Path.GetFileName(packageFile);
File.Copy(packageFile, Path.Combine(releasesDir, fileName));
});
var deltaBuilder = new DeltaPackageBuilder();
deltaBuilder.CreateDeltaPackage(firstRelease, secondRelease, firstDelta);
deltaBuilder.CreateDeltaPackage(secondRelease, thirdRelease, secondDelta);
ReleaseEntry.BuildReleasesFile(releasesDir);
var updateManager = new UpdateManager(
releasesDir, "ShimmerDesktopDemo", FrameworkVersion.Net40, installDir);
using (updateManager) {
var updateInfo = updateManager.CheckForUpdate().First();
Assert.Equal(2, updateInfo.ReleasesToApply.Count());
updateManager.DownloadReleases(updateInfo.ReleasesToApply).Wait();
updateManager.ApplyReleases(updateInfo).Wait();
}
string referenceDir;
using (IntegrationTestHelper.WithFakeAlreadyInstalledApp("InstalledSquirrelDesktopDemo-1.2.0.zip", out referenceDir)) {
var referenceVersion = Path.Combine(referenceDir, "ShimmerDesktopDemo", "app-1.2.0");
var installVersion = Path.Combine(installDir, "ShimmerDesktopDemo", "app-1.2.0");
var referenceFiles = Directory.GetFiles(referenceVersion);
var actualFiles = Directory.GetFiles(installVersion);
Assert.Equal(referenceFiles.Count(), actualFiles.Count());
var invalidFiles =
Enumerable.Zip(referenceFiles, actualFiles,
(reference, actual) => {
var refSha = Utility.CalculateFileSHA1(reference);
var actualSha = Utility.CalculateFileSHA1(actual);
return new { File = actual, Result = refSha == actualSha };
})
.Where(c => !c.Result).ToArray();
Assert.Empty(invalidFiles);
}
}
*/
}
}
public class CreateDeltaPackageTests : IEnableLogger
{
[Fact]
public void CreateDeltaPackageIntegrationTest()
{
var basePackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.1.0-pre.nupkg");
var newPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.2.0-pre.nupkg");
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
var baseFixture = new ReleasePackage(basePackage);
var fixture = new ReleasePackage(newPackage);
var tempFiles = Enumerable.Range(0, 3)
.Select(_ => Path.GetTempPath() + Guid.NewGuid().ToString() + ".nupkg")
.ToArray();
try {
baseFixture.CreateReleasePackage(tempFiles[0], sourceDir);
fixture.CreateReleasePackage(tempFiles[1], sourceDir);
(new FileInfo(baseFixture.ReleasePackageFile)).Exists.ShouldBeTrue();
(new FileInfo(fixture.ReleasePackageFile)).Exists.ShouldBeTrue();
var deltaBuilder = new DeltaPackageBuilder();
deltaBuilder.CreateDeltaPackage(baseFixture, fixture, tempFiles[2]);
var fullPkg = new ZipPackage(tempFiles[1]);
var deltaPkg = new ZipPackage(tempFiles[2]);
//
// Package Checks
//
fullPkg.Id.ShouldEqual(deltaPkg.Id);
fullPkg.Version.CompareTo(deltaPkg.Version).ShouldEqual(0);
// Delta packages should be smaller than the original!
var fileInfos = tempFiles.Select(x => new FileInfo(x)).ToArray();
this.Log().Info("Base Size: {0}, Current Size: {1}, Delta Size: {2}",
fileInfos[0].Length, fileInfos[1].Length, fileInfos[2].Length);
(fileInfos[2].Length - fileInfos[1].Length).ShouldBeLessThan(0);
//
// File Checks
///
var deltaPkgFiles = deltaPkg.GetFiles().ToList();
deltaPkgFiles.Count.ShouldBeGreaterThan(0);
this.Log().Info("Files in delta package:");
deltaPkgFiles.ForEach(x => this.Log().Info(x.Path));
var newFilesAdded = new[] {
"Newtonsoft.Json.dll",
"Refit.dll",
"Refit-Portable.dll",
"Castle.Core.dll",
}.Select(x => x.ToLowerInvariant());
// vNext adds a dependency on Refit
newFilesAdded
.All(x => deltaPkgFiles.Any(y => y.Path.ToLowerInvariant().Contains(x)))
.ShouldBeTrue();
// All the other files should be diffs and shasums
deltaPkgFiles
.Where(x => !newFilesAdded.Any(y => x.Path.ToLowerInvariant().Contains(y)))
.All(x => x.Path.ToLowerInvariant().EndsWith("diff") || x.Path.ToLowerInvariant().EndsWith("shasum"))
.ShouldBeTrue();
// Every .diff file should have a shasum file
deltaPkg.GetFiles().Any(x => x.Path.ToLowerInvariant().EndsWith(".diff")).ShouldBeTrue();
deltaPkg.GetFiles()
.Where(x => x.Path.ToLowerInvariant().EndsWith(".diff"))
.ForEach(x => {
var lookingFor = x.Path.Replace(".diff", ".shasum");
this.Log().Info("Looking for corresponding shasum file: {0}", lookingFor);
deltaPkg.GetFiles().Any(y => y.Path == lookingFor).ShouldBeTrue();
});
} finally {
tempFiles.ForEach(File.Delete);
}
}
[Fact]
public void WhenBasePackageIsNewerThanNewPackageThrowException()
{
var basePackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.2.0-pre.nupkg");
var newPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.1.0-pre.nupkg");
var sourceDir = IntegrationTestHelper.GetPath("..", "packages");
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
var baseFixture = new ReleasePackage(basePackage);
var fixture = new ReleasePackage(newPackage);
var tempFiles = Enumerable.Range(0, 3)
.Select(_ => Path.GetTempPath() + Guid.NewGuid().ToString() + ".nupkg")
.ToArray();
try {
baseFixture.CreateReleasePackage(tempFiles[0], sourceDir);
fixture.CreateReleasePackage(tempFiles[1], sourceDir);
(new FileInfo(baseFixture.ReleasePackageFile)).Exists.ShouldBeTrue();
(new FileInfo(fixture.ReleasePackageFile)).Exists.ShouldBeTrue();
Assert.Throws<InvalidOperationException>(() =>
{
var deltaBuilder = new DeltaPackageBuilder();
deltaBuilder.CreateDeltaPackage(baseFixture, fixture, tempFiles[2]);
});
} finally {
tempFiles.ForEach(File.Delete);
}
}
[Fact]
public void WhenBasePackageReleaseIsNullThrowsException()
{
var basePackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.0.0.0.nupkg");
var newPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.1.0.0.nupkg");
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
var baseFixture = new ReleasePackage(basePackage);
var fixture = new ReleasePackage(newPackage);
var tempFile = Path.GetTempPath() + Guid.NewGuid() + ".nupkg";
try {
Assert.Throws<ArgumentException>(() => {
var deltaBuilder = new DeltaPackageBuilder();
deltaBuilder.CreateDeltaPackage(baseFixture, fixture, tempFile);
});
} finally {
File.Delete(tempFile);
}
}
[Fact]
public void WhenBasePackageDoesNotExistThrowException()
{
var basePackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.1.0-pre.nupkg");
var newPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.2.0-pre.nupkg");
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
var baseFixture = new ReleasePackage(basePackage);
var fixture = new ReleasePackage(newPackage);
var tempFiles = Enumerable.Range(0, 3)
.Select(_ => Path.GetTempPath() + Guid.NewGuid().ToString() + ".nupkg")
.ToArray();
try {
baseFixture.CreateReleasePackage(tempFiles[0], sourceDir);
fixture.CreateReleasePackage(tempFiles[1], sourceDir);
(new FileInfo(baseFixture.ReleasePackageFile)).Exists.ShouldBeTrue();
(new FileInfo(fixture.ReleasePackageFile)).Exists.ShouldBeTrue();
// NOW WATCH AS THE FILE DISAPPEARS
File.Delete(baseFixture.ReleasePackageFile);
Assert.Throws<FileNotFoundException>(() => {
var deltaBuilder = new DeltaPackageBuilder();
deltaBuilder.CreateDeltaPackage(baseFixture, fixture, tempFiles[2]);
});
} finally {
tempFiles.ForEach(File.Delete);
}
}
[Fact]
public void WhenNewPackageDoesNotExistThrowException()
{
var basePackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.1.0-pre.nupkg");
var newPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.2.0-pre.nupkg");
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
var baseFixture = new ReleasePackage(basePackage);
var fixture = new ReleasePackage(newPackage);
var tempFiles = Enumerable.Range(0, 3)
.Select(_ => Path.GetTempPath() + Guid.NewGuid().ToString() + ".nupkg")
.ToArray();
try {
baseFixture.CreateReleasePackage(tempFiles[0], sourceDir);
fixture.CreateReleasePackage(tempFiles[1], sourceDir);
(new FileInfo(baseFixture.ReleasePackageFile)).Exists.ShouldBeTrue();
(new FileInfo(fixture.ReleasePackageFile)).Exists.ShouldBeTrue();
// NOW WATCH AS THE FILE DISAPPEARS
File.Delete(fixture.ReleasePackageFile);
Assert.Throws<FileNotFoundException>(() => {
var deltaBuilder = new DeltaPackageBuilder();
deltaBuilder.CreateDeltaPackage(baseFixture, fixture, tempFiles[2]);
});
} finally {
tempFiles.ForEach(File.Delete);
}
}
}
}

View File

@@ -0,0 +1,37 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("Squirrel.Tests")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("Squirrel.Tests")]
[assembly: AssemblyCopyright("Copyright © 2014")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
// The following GUID is for the ID of the typelib if this project is exposed to COM
[assembly: Guid("f781bbe0-d19d-41aa-a78b-c689b1943094")]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.0.0.0")]
[assembly: AssemblyFileVersion("1.0.0.0")]
[assembly: AssemblyInformationalVersion("0.1.0-pre")]

191
test/ReleaseEntryTests.cs Normal file
View File

@@ -0,0 +1,191 @@
using System;
using System.IO;
using System.Linq;
using Squirrel;
using Squirrel.Tests.TestHelpers;
using Xunit;
namespace Squirrel.Tests.Core
{
public class ReleaseEntryTests
{
[Theory]
[InlineData("94689fede03fed7ab59c24337673a27837f0c3ec MyCoolApp-1.0.nupkg 1004502", "MyCoolApp-1.0.nupkg", 1004502)]
[InlineData("3a2eadd15dd984e4559f2b4d790ec8badaeb6a39 MyCoolApp-1.1.nupkg 1040561", "MyCoolApp-1.1.nupkg", 1040561)]
[InlineData("14db31d2647c6d2284882a2e101924a9c409ee67 MyCoolApp-1.1.nupkg.delta 80396", "MyCoolApp-1.1.nupkg.delta", 80396)]
public void ParseValidReleaseEntryLines(string releaseEntry, string fileName, long fileSize)
{
var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry);
Assert.Equal(fileName, fixture.Filename);
Assert.Equal(fileSize, fixture.Filesize);
}
[Theory]
[InlineData("Squirrel.Core.1.0.0.0.nupkg", 4457, "75255cfd229a1ed1447abe1104f5635e69975d30")]
[InlineData("Squirrel.Core.1.1.0.0.nupkg", 15830, "9baf1dbacb09940086c8c62d9a9dbe69fe1f7593")]
public void GenerateFromFileTest(string name, long size, string sha1)
{
var path = IntegrationTestHelper.GetPath("fixtures", name);
using (var f = File.OpenRead(path)) {
var fixture = ReleaseEntry.GenerateFromFile(f, "dontcare");
Assert.Equal(size, fixture.Filesize);
Assert.Equal(sha1, fixture.SHA1.ToLowerInvariant());
}
}
[Theory]
[InlineData("94689fede03fed7ab59c24337673a27837f0c3ec MyCoolApp-1.0.nupkg 1004502", 1, 0)]
[InlineData("3a2eadd15dd984e4559f2b4d790ec8badaeb6a39 MyCoolApp-1.1.nupkg 1040561", 1, 1)]
[InlineData("14db31d2647c6d2284882a2e101924a9c409ee67 MyCoolApp-1.1-delta.nupkg 80396", 1, 1)]
public void ParseVersionTest(string releaseEntry, int expectedMajor, int expectedMinor)
{
var fixture = ReleaseEntry.ParseReleaseEntry(releaseEntry);
Assert.Equal(expectedMajor, fixture.Version.Major);
Assert.Equal(expectedMinor, fixture.Version.Minor);
}
[Fact]
public void CanParseGeneratedReleaseEntryAsString()
{
var path = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.1.0.0.nupkg");
var entryAsString = ReleaseEntry.GenerateFromFile(path).EntryAsString;
ReleaseEntry.ParseReleaseEntry(entryAsString);
}
[Fact]
public void InvalidReleaseNotesThrowsException()
{
var path = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.0.0.0.nupkg");
var fixture = ReleaseEntry.GenerateFromFile(path);
Assert.Throws<Exception>(() => fixture.GetReleaseNotes(IntegrationTestHelper.GetPath("fixtures")));
}
[Fact]
public void GetLatestReleaseWithNullCollectionReturnsNull()
{
Assert.Null(ReleaseEntry.GetPreviousRelease(
null, null, null));
}
[Fact]
public void GetLatestReleaseWithEmptyCollectionReturnsNull()
{
Assert.Null(ReleaseEntry.GetPreviousRelease(
Enumerable.Empty<ReleaseEntry>(), null, null));
}
[Fact]
public void WhenCurrentReleaseMatchesLastReleaseReturnNull()
{
var package = new ReleasePackage("Espera-1.7.6-beta.nupkg");
var releaseEntries = new[] {
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg"))
};
Assert.Null(ReleaseEntry.GetPreviousRelease(
releaseEntries, package, @"C:\temp\somefolder"));
}
[Fact]
public void WhenMultipleReleaseMatchesReturnEarlierResult()
{
var expected = new Version("1.7.5");
var package = new ReleasePackage("Espera-1.7.6-beta.nupkg");
var releaseEntries = new[] {
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")),
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.5-beta.nupkg"))
};
var actual = ReleaseEntry.GetPreviousRelease(
releaseEntries,
package,
@"C:\temp\");
Assert.Equal(expected, actual.Version);
}
[Fact]
public void WhenMultipleReleasesFoundReturnPreviousVersion()
{
var expected = new Version("1.7.6");
var input = new ReleasePackage("Espera-1.7.7-beta.nupkg");
var releaseEntries = new[] {
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg")),
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.5-beta.nupkg"))
};
var actual = ReleaseEntry.GetPreviousRelease(
releaseEntries,
input,
@"C:\temp\");
Assert.Equal(expected, actual.Version);
}
[Fact]
public void WhenMultipleReleasesFoundInOtherOrderReturnPreviousVersion()
{
var expected = new Version("1.7.6");
var input = new ReleasePackage("Espera-1.7.7-beta.nupkg");
var releaseEntries = new[] {
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.5-beta.nupkg")),
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.7.6-beta.nupkg"))
};
var actual = ReleaseEntry.GetPreviousRelease(
releaseEntries,
input,
@"C:\temp\");
Assert.Equal(expected, actual.Version);
}
[Fact]
public void WhenReleasesAreOutOfOrderSortByVersion()
{
var path = Path.GetTempFileName();
var firstVersion = new Version("1.0.0");
var secondVersion = new Version("1.1.0");
var thirdVersion = new Version("1.2.0");
var releaseEntries = new[] {
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-delta.nupkg")),
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-delta.nupkg")),
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.1.0-full.nupkg")),
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.2.0-full.nupkg")),
ReleaseEntry.ParseReleaseEntry(MockReleaseEntry("Espera-1.0.0-full.nupkg"))
};
ReleaseEntry.WriteReleaseFile(releaseEntries, path);
var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(path)).ToArray();
Assert.Equal(firstVersion, releases[0].Version);
Assert.Equal(secondVersion, releases[1].Version);
Assert.Equal(true, releases[1].IsDelta);
Assert.Equal(secondVersion, releases[2].Version);
Assert.Equal(false, releases[2].IsDelta);
Assert.Equal(thirdVersion, releases[3].Version);
Assert.Equal(true, releases[3].IsDelta);
Assert.Equal(thirdVersion, releases[4].Version);
Assert.Equal(false, releases[4].IsDelta);
}
[Fact]
public void ParseReleaseFileShouldReturnNothingForBlankFiles()
{
Assert.True(ReleaseEntry.ParseReleaseFile("").Count() == 0);
Assert.True(ReleaseEntry.ParseReleaseFile(null).Count() == 0);
}
static string MockReleaseEntry(string name)
{
return string.Format("94689fede03fed7ab59c24337673a27837f0c3ec {0} 1004502", name);
}
}
}

327
test/ReleasePackageTests.cs Normal file
View File

@@ -0,0 +1,327 @@
using System.Runtime.Versioning;
using MarkdownSharp;
using NuGet;
using Squirrel;
using Squirrel.Tests.TestHelpers;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Xml.Linq;
using Splat;
using Xunit;
namespace Squirrel.Tests.Core
{
public class CreateReleasePackageTests : IEnableLogger
{
[Fact]
public void ReleasePackageIntegrationTest()
{
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.1.0-pre.nupkg");
var outputPackage = Path.GetTempFileName() + ".nupkg";
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
var fixture = new ReleasePackage(inputPackage);
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
try {
fixture.CreateReleasePackage(outputPackage, sourceDir);
this.Log().Info("Resulting package is at {0}", outputPackage);
var pkg = new ZipPackage(outputPackage);
int refs = pkg.FrameworkAssemblies.Count();
this.Log().Info("Found {0} refs", refs);
refs.ShouldEqual(0);
this.Log().Info("Files in release package:");
List<IPackageFile> files = pkg.GetFiles().ToList();
files.ForEach(x => this.Log().Info(x.Path));
List<string> nonDesktopPaths = new[] {"sl", "winrt", "netcore", "win8", "windows8", "MonoAndroid", "MonoTouch", "MonoMac", "wp", }
.Select(x => @"lib\" + x)
.ToList();
files.Any(x => nonDesktopPaths.Any(y => x.Path.ToLowerInvariant().Contains(y.ToLowerInvariant()))).ShouldBeFalse();
files.Any(x => x.Path.ToLowerInvariant().EndsWith(@".xml")).ShouldBeFalse();
} finally {
File.Delete(outputPackage);
}
}
[Fact]
public void FindPackageInOurLocalPackageList()
{
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.0.0.0.nupkg");
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
var fixture = ExposedObject.From(new ReleasePackage(inputPackage));
IPackage result = fixture.matchPackage(new LocalPackageRepository(sourceDir), "xunit", VersionUtility.ParseVersionSpec("[1.0,2.0]"));
result.Id.ShouldEqual("xunit");
result.Version.Version.Major.ShouldEqual(2);
result.Version.Version.Minor.ShouldEqual(0);
}
[Fact]
public void FindDependentPackagesForDummyPackage()
{
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.1.0-pre.nupkg");
var fixture = new ReleasePackage(inputPackage);
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
IEnumerable<IPackage> results = fixture.findAllDependentPackages(default(IPackage), (IPackageRepository)new LocalPackageRepository(sourceDir), default(HashSet<string>), default(FrameworkName));
results.Count().ShouldBeGreaterThan(0);
}
[Fact]
public void CanLoadPackageWhichHasNoDependencies()
{
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.NoDependencies.1.0.0.0.nupkg");
var outputPackage = Path.GetTempFileName() + ".nupkg";
var fixture = new ReleasePackage(inputPackage);
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
try {
fixture.CreateReleasePackage(outputPackage, sourceDir);
}
finally {
File.Delete(outputPackage);
}
}
[Fact]
public void CanResolveMultipleLevelsOfDependencies()
{
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Tests.0.1.0-pre.nupkg");
var outputPackage = Path.GetTempFileName() + ".nupkg";
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
var fixture = new ReleasePackage(inputPackage);
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
try {
fixture.CreateReleasePackage(outputPackage, sourceDir);
this.Log().Info("Resulting package is at {0}", outputPackage);
var pkg = new ZipPackage(outputPackage);
int refs = pkg.FrameworkAssemblies.Count();
this.Log().Info("Found {0} refs", refs);
refs.ShouldEqual(0);
this.Log().Info("Files in release package:");
pkg.GetFiles().ForEach(x => this.Log().Info(x.Path));
var filesToLookFor = new[] {
"xunit.assert.dll", // Tests => Xunit => Xunit.Assert
"NuGet.Core.dll", // Tests => NuGet
"Squirrel.Tests.dll",
};
filesToLookFor.ForEach(name => {
this.Log().Info("Looking for {0}", name);
pkg.GetFiles().Any(y => y.Path.ToLowerInvariant().Contains(name.ToLowerInvariant())).ShouldBeTrue();
});
} finally {
File.Delete(outputPackage);
}
}
[Fact]
public void SpecFileMarkdownRenderingTest()
{
var dontcare = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.1.0.0.nupkg");
var inputSpec = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.1.0.0.nuspec");
var fixture = new ReleasePackage(dontcare);
var targetFile = Path.GetTempFileName();
File.Copy(inputSpec, targetFile, true);
try {
var processor = new Func<string, string>(input =>
(new Markdown()).Transform(input));
// NB: For No Reason At All, renderReleaseNotesMarkdown is
// invulnerable to ExposedObject. Whyyyyyyyyy
var renderMinfo = fixture.GetType().GetMethod("renderReleaseNotesMarkdown",
BindingFlags.NonPublic | BindingFlags.Instance);
renderMinfo.Invoke(fixture, new object[] {targetFile, processor});
var doc = XDocument.Load(targetFile);
XNamespace ns = "http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd";
var relNotesElement = doc.Descendants(ns + "releaseNotes").First();
var htmlText = relNotesElement.Value;
this.Log().Info("HTML Text:\n{0}", htmlText);
htmlText.Contains("## Release Notes").ShouldBeFalse();
} finally {
File.Delete(targetFile);
}
}
[Fact]
public void UsesTheRightVersionOfADependencyWhenMultipleAreInPackages()
{
var outputPackage = Path.GetTempFileName() + ".nupkg";
string outputFile = null;
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "CaliburnMicroDemo.1.0.0.nupkg");
var wrongPackage = "Caliburn.Micro.1.4.1.nupkg";
var wrongPackagePath = IntegrationTestHelper.GetPath("fixtures", wrongPackage);
var rightPackage = "Caliburn.Micro.1.5.2.nupkg";
var rightPackagePath = IntegrationTestHelper.GetPath("fixtures", rightPackage);
try {
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
File.Copy(wrongPackagePath, Path.Combine(sourceDir, wrongPackage), true);
File.Copy(rightPackagePath, Path.Combine(sourceDir, rightPackage), true);
var package = new ReleasePackage(inputPackage);
var outputFileName = package.CreateReleasePackage(outputPackage, sourceDir);
var zipPackage = new ZipPackage(outputFileName);
var fileName = "Caliburn.Micro.dll";
var dependency = zipPackage.GetLibFiles()
.Where(f => f.Path.EndsWith(fileName))
.Single(f => f.TargetFramework == FrameworkTargetVersion.Net40);
outputFile = new FileInfo(Path.Combine(sourceDir, fileName)).FullName;
using (var of = File.Create(outputFile))
{
dependency.GetStream().CopyTo(of);
}
var assemblyName = AssemblyName.GetAssemblyName(outputFile);
Assert.Equal(1, assemblyName.Version.Major);
Assert.Equal(5, assemblyName.Version.Minor);
}
finally {
File.Delete(outputPackage);
File.Delete(outputFile);
}
}
[Fact]
public void DependentPackageNotFoundAndThrowsError()
{
string packagesDir;
// use empty packages folder
using (Utility.WithTempDirectory(out packagesDir)) {
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "ProjectDependsOnJsonDotNet.1.0.nupkg");
var outputPackage = Path.GetTempFileName() + ".nupkg";
try {
var package = new ReleasePackage(inputPackage);
Assert.Throws<Exception>(() =>
package.CreateReleasePackage(outputPackage, packagesDir));
} finally {
File.Delete(outputPackage);
}
}
}
[Fact]
public void DependentLocalPackageNotFoundAndThrowsError()
{
// copy ProjectDependsOnOtherProject to a temp folder
// create a release package using it
// should throw an exception indicating it can't find TheOtherProjectItDependsOn.1.0.nupkg
Assert.False(true, "Rewrite this test");
}
[Fact]
public void DependentLocalPackageFoundAndIncludedInReleasePackage()
{
// copy ProjectDependsOnOtherProject and TheOtherProjectItDependsOn to a temp folder
// create a release package using it
// should contain TheOtherProjectItDependsOn.dll
// XXX: what about scenario where it is in another folder?
Assert.False(true, "Rewrite this test");
}
[Fact]
public void ContentFilesAreIncludedInCreatedPackage()
{
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "ProjectWithContent.1.0.0.0-beta.nupkg");
var outputPackage = Path.GetTempFileName() + ".nupkg";
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
var fixture = new ReleasePackage(inputPackage);
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
try
{
fixture.CreateReleasePackage(outputPackage, sourceDir);
this.Log().Info("Resulting package is at {0}", outputPackage);
var pkg = new ZipPackage(outputPackage);
int refs = pkg.FrameworkAssemblies.Count();
this.Log().Info("Found {0} refs", refs);
refs.ShouldEqual(0);
this.Log().Info("Files in release package:");
var contentFiles = pkg.GetContentFiles();
Assert.Equal(2, contentFiles.Count());
var contentFilePaths = contentFiles.Select(f => f.EffectivePath);
Assert.Contains("some-words.txt", contentFilePaths);
Assert.Contains("dir\\item-in-subdirectory.txt", contentFilePaths);
Assert.Equal(1, pkg.GetLibFiles().Count());
}
finally
{
File.Delete(outputPackage);
}
}
[Fact]
public void WhenAProjectContainsNet45BinariesItContainsTheNecessaryDependency()
{
var outputPackage = Path.GetTempFileName() + ".nupkg";
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "ThisShouldBeANet45Project.1.0.nupkg");
var rightPackage = "Caliburn.Micro.1.5.2.nupkg";
var rightPackagePath = IntegrationTestHelper.GetPath("fixtures", rightPackage);
try
{
var sourceDir = IntegrationTestHelper.GetPath("fixtures", "packages");
(new DirectoryInfo(sourceDir)).Exists.ShouldBeTrue();
File.Copy(rightPackagePath, Path.Combine(sourceDir, rightPackage), true);
var package = new ReleasePackage(inputPackage);
var outputFileName = package.CreateReleasePackage(outputPackage, sourceDir);
var zipPackage = new ZipPackage(outputFileName);
var dependency = zipPackage.GetLibFiles()
.Where(f => f.Path.EndsWith("Caliburn.Micro.dll"))
.FirstOrDefault(f => f.TargetFramework == FrameworkTargetVersion.Net45);
Assert.NotNull(dependency);
}
finally
{
File.Delete(outputPackage);
}
}
}
}

111
test/Squirrel.Tests.csproj Normal file
View File

@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\packages\xunit.core.2.0.0-beta-build2700\build\portable-net45+win+wpa81+wp80\xunit.core.props" Condition="Exists('..\packages\xunit.core.2.0.0-beta-build2700\build\portable-net45+win+wpa81+wp80\xunit.core.props')" />
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProjectGuid>{98AEB048-E27D-42F4-9440-505B7F78BAFD}</ProjectGuid>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Squirrel.Tests</RootNamespace>
<AssemblyName>Squirrel.Tests</AssemblyName>
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
<FileAlignment>512</FileAlignment>
<NuGetPackageImportStamp>2c7138af</NuGetPackageImportStamp>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\Debug\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\Release\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
</PropertyGroup>
<ItemGroup>
<Reference Include="Ionic.Zip">
<HintPath>..\ext\Ionic.Zip.dll</HintPath>
</Reference>
<Reference Include="Microsoft.Web.XmlTransform">
<HintPath>..\packages\Microsoft.Web.Xdt.2.1.1\lib\net40\Microsoft.Web.XmlTransform.dll</HintPath>
</Reference>
<Reference Include="NuGet.Core, Version=2.8.50506.491, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\packages\NuGet.Core.2.8.2\lib\net40-Client\NuGet.Core.dll</HintPath>
</Reference>
<Reference Include="Splat">
<HintPath>..\packages\Splat.1.4.0\lib\Net45\Splat.dll</HintPath>
</Reference>
<Reference Include="System" />
<Reference Include="System.Core" />
<Reference Include="System.Xml.Linq" />
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Data" />
<Reference Include="System.Xml" />
<Reference Include="xunit.abstractions">
<HintPath>..\packages\xunit.abstractions.2.0.0-beta-build2700\lib\net35\xunit.abstractions.dll</HintPath>
</Reference>
<Reference Include="xunit.assert">
<HintPath>..\packages\xunit.assert.2.0.0-beta-build2700\lib\portable-net45+win+wpa81+wp80+monoandroid+monotouch10\xunit.assert.dll</HintPath>
</Reference>
<Reference Include="xunit.core">
<HintPath>..\packages\xunit.core.2.0.0-beta-build2700\lib\portable-net45+win+wpa81+wp80+monotouch+monoandroid\xunit.core.dll</HintPath>
</Reference>
<Reference Include="xunit.runner.utility">
<HintPath>..\packages\xunit.runner.utility.2.0.0-beta-build2700\lib\net35\xunit.runner.utility.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Include="ContentTypeTests.cs" />
<Compile Include="DeltaPackageTests.cs" />
<Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="ReleaseEntryTests.cs" />
<Compile Include="ReleasePackageTests.cs" />
<Compile Include="TestHelpers\AssertExtensions.cs" />
<Compile Include="TestHelpers\ExposedClass.cs" />
<Compile Include="TestHelpers\ExposedObject.cs" />
<Compile Include="TestHelpers\ExposedObjectHelper.cs" />
<Compile Include="TestHelpers\IntegrationTestHelper.cs" />
<Compile Include="TestHelpers\StaticHttpServer.cs" />
<Compile Include="UtilityTests.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\src\Squirrel.csproj">
<Project>{1436e22a-fe3c-4d68-9a85-9e74df2e6a92}</Project>
<Name>Squirrel</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<Service Include="{82A7F48D-3B50-4B1E-B82E-3ADA8210C358}" />
</ItemGroup>
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Enable NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\packages\xunit.core.2.0.0-beta-build2700\build\portable-net45+win+wpa81+wp80\xunit.core.props')" Text="$([System.String]::Format('$(ErrorText)', '..\packages\xunit.core.2.0.0-beta-build2700\build\portable-net45+win+wpa81+wp80\xunit.core.props'))" />
</Target>
<PropertyGroup>
<PostBuildEvent>"$(SolutionDir).nuget\NuGet.exe" pack "$(ProjectPath)" -OutputDirectory "$(ProjectDir)$(OutDir)."</PostBuildEvent>
</PropertyGroup>
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target>
<Target Name="AfterBuild">
</Target>
-->
</Project>

View File

@@ -0,0 +1,152 @@
using System;
using System.Collections;
using System.Globalization;
using System.IO;
using Xunit;
namespace Squirrel.Tests.TestHelpers
{
public static class AssertExtensions
{
public static void ShouldBeAboutEqualTo(this DateTimeOffset expected, DateTimeOffset current)
{
Assert.Equal(expected.Date, current.Date);
Assert.Equal(expected.Offset, current.Offset);
Assert.Equal(expected.Hour, current.Hour);
Assert.Equal(expected.Minute, current.Minute);
Assert.Equal(expected.Second, current.Second);
}
public static void ShouldBeFalse(this bool currentObject)
{
Assert.False(currentObject);
}
public static void ShouldBeNull(this object currentObject)
{
Assert.Null(currentObject);
}
public static void ShouldBeEmpty(this IEnumerable items)
{
Assert.Empty(items);
}
public static void ShouldNotBeEmpty(this IEnumerable items)
{
Assert.NotEmpty(items);
}
public static void ShouldBeTrue(this bool currentObject)
{
Assert.True(currentObject);
}
public static void ShouldEqual(this object compareFrom, object compareTo)
{
Assert.Equal(compareTo, compareFrom);
}
public static void ShouldEqual<T>(this T compareFrom, T compareTo)
{
Assert.Equal(compareTo, compareFrom);
}
public static void ShouldBeSameAs<T>(this T actual, T expected)
{
Assert.Same(expected, actual);
}
public static void ShouldNotBeSameAs<T>(this T actual, T expected)
{
Assert.NotSame(expected, actual);
}
public static void ShouldBeAssignableFrom<T>(this object instance) where T : class
{
Assert.IsAssignableFrom<T>(instance);
}
public static void ShouldBeType(this object instance, Type type)
{
Assert.IsType(type, instance);
}
public static void ShouldBeType<T>(this object instance)
{
Assert.IsType<T>(instance);
}
public static void ShouldNotBeType<T>(this object instance)
{
Assert.IsNotType<T>(instance);
}
public static void ShouldContain(this string current, string expectedSubstring, StringComparison comparison)
{
Assert.Contains(expectedSubstring, current, comparison);
}
public static void ShouldStartWith(this string current, string expectedSubstring, StringComparison comparison)
{
Assert.True(current.StartsWith(expectedSubstring, comparison));
}
public static void ShouldNotBeNull(this object currentObject)
{
Assert.NotNull(currentObject);
}
public static void ShouldNotBeNullNorEmpty(this string value)
{
Assert.NotNull(value);
Assert.NotEmpty(value);
}
public static void ShouldNotEqual(this object compareFrom, object compareTo)
{
Assert.NotEqual(compareTo, compareFrom);
}
public static void ShouldBeGreaterThan<T>(this T current, T other) where T : IComparable
{
Assert.True(current.CompareTo(other) > 0, current + " is not greater than " + other);
}
public static void ShouldBeLessThan<T>(this T current, T other) where T : IComparable
{
Assert.True(current.CompareTo(other) < 0, current + " is not less than " + other);
}
static string ToSafeString(this char c)
{
if (Char.IsControl(c) || Char.IsWhiteSpace(c))
{
switch (c)
{
case '\r':
return @"\r";
case '\n':
return @"\n";
case '\t':
return @"\t";
case '\a':
return @"\a";
case '\v':
return @"\v";
case '\f':
return @"\f";
default:
return String.Format("\\u{0:X};", (int)c);
}
}
return c.ToString(CultureInfo.InvariantCulture);
}
}
public enum DiffStyle
{
Full,
Minimal
}
}

View File

@@ -0,0 +1,138 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Dynamic;
using System.Reflection;
// Lovingly stolen from http://exposedobject.codeplex.com/
namespace Squirrel.Tests.TestHelpers
{
public class ExposedClass : DynamicObject
{
private Type m_type;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> m_staticMethods;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> m_genStaticMethods;
private ExposedClass(Type type)
{
m_type = type;
m_staticMethods =
m_type
.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)
.Where(m => !m.IsGenericMethod)
.GroupBy(m => m.Name)
.ToDictionary(
p => p.Key,
p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList()));
m_genStaticMethods =
m_type
.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static)
.Where(m => m.IsGenericMethod)
.GroupBy(m => m.Name)
.ToDictionary(
p => p.Key,
p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList()));
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
// Get type args of the call
Type[] typeArgs = ExposedObjectHelper.GetTypeArgs(binder);
if (typeArgs != null && typeArgs.Length == 0) typeArgs = null;
//
// Try to call a non-generic instance method
//
if (typeArgs == null
&& m_staticMethods.ContainsKey(binder.Name)
&& m_staticMethods[binder.Name].ContainsKey(args.Length)
&& ExposedObjectHelper.InvokeBestMethod(args, null, m_staticMethods[binder.Name][args.Length], out result))
{
return true;
}
//
// Try to call a generic instance method
//
if (m_staticMethods.ContainsKey(binder.Name)
&& m_staticMethods[binder.Name].ContainsKey(args.Length))
{
List<MethodInfo> methods = new List<MethodInfo>();
foreach (var method in m_genStaticMethods[binder.Name][args.Length])
{
if (method.GetGenericArguments().Length == typeArgs.Length)
{
methods.Add(method.MakeGenericMethod(typeArgs));
}
}
if (ExposedObjectHelper.InvokeBestMethod(args, null, methods, out result))
{
return true;
}
}
result = null;
return false;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
var propertyInfo = m_type.GetProperty(
binder.Name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (propertyInfo != null)
{
propertyInfo.SetValue(null, value, null);
return true;
}
var fieldInfo = m_type.GetField(
binder.Name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (fieldInfo != null)
{
fieldInfo.SetValue(null, value);
return true;
}
return false;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
var propertyInfo = m_type.GetProperty(
binder.Name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (propertyInfo != null)
{
result = propertyInfo.GetValue(null, null);
return true;
}
var fieldInfo = m_type.GetField(
binder.Name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Static);
if (fieldInfo != null)
{
result = fieldInfo.GetValue(null);
return true;
}
result = null;
return false;
}
public static dynamic From(Type type)
{
return new ExposedClass(type);
}
}
}

View File

@@ -0,0 +1,170 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Dynamic;
using System.Reflection;
// Lovingly stolen from http://exposedobject.codeplex.com/
namespace Squirrel.Tests.TestHelpers
{
public class ExposedObject : DynamicObject
{
private object m_object;
private Type m_type;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> m_instanceMethods;
private Dictionary<string, Dictionary<int, List<MethodInfo>>> m_genInstanceMethods;
private ExposedObject(object obj)
{
m_object = obj;
m_type = obj.GetType();
m_instanceMethods =
m_type
.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Where(m => !m.IsGenericMethod)
.GroupBy(m => m.Name)
.ToDictionary(
p => p.Key,
p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList()));
m_genInstanceMethods =
m_type
.GetMethods(BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance)
.Where(m => m.IsGenericMethod)
.GroupBy(m => m.Name)
.ToDictionary(
p => p.Key,
p => p.GroupBy(r => r.GetParameters().Length).ToDictionary(r => r.Key, r => r.ToList()));
}
public object Object { get { return m_object; } }
public static dynamic New<T>()
{
return New(typeof(T));
}
public static dynamic New(Type type)
{
return new ExposedObject(Activator.CreateInstance(type));
}
public static dynamic From(object obj)
{
return new ExposedObject(obj);
}
public static T Cast<T>(ExposedObject t)
{
return (T)t.m_object;
}
public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result)
{
// Get type args of the call
Type[] typeArgs = ExposedObjectHelper.GetTypeArgs(binder);
if (typeArgs != null && typeArgs.Length == 0) typeArgs = null;
//
// Try to call a non-generic instance method
//
if (typeArgs == null
&& m_instanceMethods.ContainsKey(binder.Name)
&& m_instanceMethods[binder.Name].ContainsKey(args.Length)
&& ExposedObjectHelper.InvokeBestMethod(args, m_object, m_instanceMethods[binder.Name][args.Length], out result))
{
return true;
}
//
// Try to call a generic instance method
//
if (m_instanceMethods.ContainsKey(binder.Name)
&& m_instanceMethods[binder.Name].ContainsKey(args.Length))
{
List<MethodInfo> methods = new List<MethodInfo>();
if (m_genInstanceMethods.ContainsKey(binder.Name) &&
m_genInstanceMethods[binder.Name].ContainsKey(args.Length))
{
foreach (var method in m_genInstanceMethods[binder.Name][args.Length])
{
if (method.GetGenericArguments().Length == typeArgs.Length)
{
methods.Add(method.MakeGenericMethod(typeArgs));
}
}
}
if (ExposedObjectHelper.InvokeBestMethod(args, m_object, methods, out result))
{
return true;
}
}
result = null;
return false;
}
public override bool TrySetMember(SetMemberBinder binder, object value)
{
var propertyInfo = m_type.GetProperty(
binder.Name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
if (propertyInfo != null)
{
propertyInfo.SetValue(m_object, value, null);
return true;
}
var fieldInfo = m_type.GetField(
binder.Name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
if (fieldInfo != null)
{
fieldInfo.SetValue(m_object, value);
return true;
}
return false;
}
public override bool TryGetMember(GetMemberBinder binder, out object result)
{
var propertyInfo = m_object.GetType().GetProperty(
binder.Name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
if (propertyInfo != null)
{
result = propertyInfo.GetValue(m_object, null);
return true;
}
var fieldInfo = m_object.GetType().GetField(
binder.Name,
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
if (fieldInfo != null)
{
result = fieldInfo.GetValue(m_object);
return true;
}
result = null;
return false;
}
public override bool TryConvert(ConvertBinder binder, out object result)
{
result = m_object;
return true;
}
}
}

View File

@@ -0,0 +1,94 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Dynamic;
// Lovingly stolen from http://exposedobject.codeplex.com/
namespace Squirrel.Tests.TestHelpers
{
internal class ExposedObjectHelper
{
private static Type s_csharpInvokePropertyType =
typeof(Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)
.Assembly
.GetType("Microsoft.CSharp.RuntimeBinder.ICSharpInvokeOrInvokeMemberBinder");
internal static bool InvokeBestMethod(object[] args, object target, List<MethodInfo> instanceMethods, out object result)
{
if (instanceMethods.Count == 1)
{
// Just one matching instance method - call it
if (TryInvoke(instanceMethods[0], target, args, out result))
{
return true;
}
}
else if (instanceMethods.Count > 1)
{
// Find a method with best matching parameters
MethodInfo best = null;
Type[] bestParams = null;
Type[] actualParams = args.Select(p => p == null ? typeof(object) : p.GetType()).ToArray();
Func<Type[], Type[], bool> isAssignableFrom = (a, b) =>
{
for (int i = 0; i < a.Length; i++)
{
if (!a[i].IsAssignableFrom(b[i])) return false;
}
return true;
};
foreach (var method in instanceMethods.Where(m => m.GetParameters().Length == args.Length))
{
Type[] mParams = method.GetParameters().Select(x => x.ParameterType).ToArray();
if (isAssignableFrom(mParams, actualParams))
{
if (best == null || isAssignableFrom(bestParams, mParams))
{
best = method;
bestParams = mParams;
}
}
}
if (best != null && TryInvoke(best, target, args, out result))
{
return true;
}
}
result = null;
return false;
}
internal static bool TryInvoke(MethodInfo methodInfo, object target, object[] args, out object result)
{
try
{
result = methodInfo.Invoke(target, args);
return true;
}
catch (TargetInvocationException) { }
catch (TargetParameterCountException) { }
result = null;
return false;
}
internal static Type[] GetTypeArgs(InvokeMemberBinder binder)
{
if (s_csharpInvokePropertyType.IsInstanceOfType(binder))
{
PropertyInfo typeArgsProperty = s_csharpInvokePropertyType.GetProperty("TypeArguments");
return ((IEnumerable<Type>)typeArgsProperty.GetValue(binder, null)).ToArray();
}
return null;
}
}
}

View File

@@ -0,0 +1,102 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading;
using Ionic.Zip;
using Squirrel;
using Splat;
namespace Squirrel.Tests.TestHelpers
{
public static class IntegrationTestHelper
{
public static string GetPath(params string[] paths)
{
var ret = GetIntegrationTestRootDirectory();
return (new FileInfo(paths.Aggregate (ret, Path.Combine))).FullName;
}
public static string GetIntegrationTestRootDirectory()
{
// XXX: This is an evil hack, but it's okay for a unit test
// We can't use Assembly.Location because unit test runners love
// to move stuff to temp directories
var st = new StackFrame(true);
var di = new DirectoryInfo(Path.Combine(Path.GetDirectoryName(st.GetFileName()), ".."));
return di.FullName;
}
public static bool SkipTestOnXPAndVista()
{
int osVersion = Environment.OSVersion.Version.Major*100 + Environment.OSVersion.Version.Minor;
return (osVersion < 601);
}
public static void RunBlockAsSTA(Action block)
{
Exception ex = null;
var t = new Thread(() => {
try {
block();
} catch (Exception e) {
ex = e;
}
});
t.SetApartmentState(ApartmentState.STA);
t.Start();
t.Join();
if (ex != null) {
// NB: If we don't do this, the test silently passes
throw new Exception("", ex);
}
}
static object gate = 42;
public static IDisposable WithFakeInstallDirectory(string packageFileName, out string path)
{
var ret = Utility.WithTempDirectory(out path);
File.Copy(GetPath("fixtures", packageFileName), Path.Combine(path, packageFileName));
var rp = ReleaseEntry.GenerateFromFile(Path.Combine(path, packageFileName));
ReleaseEntry.WriteReleaseFile(new[] { rp }, Path.Combine(path, "RELEASES"));
// NB: This is a temporary hack. The reason we serialize the tests
// like this, is to make sure that we don't have two tests registering
// their Service Locators with RxApp.
Monitor.Enter(gate);
return Disposable.Create(() => {
ret.Dispose();
Monitor.Exit(gate);
});
}
public static IDisposable WithFakeInstallDirectory(out string path)
{
return WithFakeInstallDirectory("SampleUpdatingApp.1.1.0.0.nupkg", out path);
}
public static IDisposable WithFakeAlreadyInstalledApp(out string path)
{
return WithFakeAlreadyInstalledApp("InstalledSampleUpdatingApp-1.1.0.0.zip", out path);
}
public static IDisposable WithFakeAlreadyInstalledApp(string zipFile, out string path)
{
var ret = Utility.WithTempDirectory(out path);
var zf = new ZipFile(GetPath("fixtures", zipFile));
zf.ExtractAll(path);
Monitor.Enter(gate);
return Disposable.Create(() => {
ret.Dispose();
Monitor.Exit(gate);
});
}
}
}

View File

@@ -0,0 +1,96 @@
using System;
using System.IO;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
namespace Squirrel.Tests
{
public sealed class StaticHttpServer : IDisposable
{
public int Port { get; private set; }
public string RootPath { get; private set; }
IDisposable inner;
public StaticHttpServer(int port, string rootPath)
{
Port = port; RootPath = rootPath;
}
public IDisposable Start()
{
if (inner != null) {
throw new InvalidOperationException("Already started!");
}
var server = new HttpListener();
server.Prefixes.Add(String.Format("http://+:{0}/", Port));
server.Start();
bool shouldStop = false;
var listener = Task.Run(async () => {
while (!shouldStop) {
var ctx = await server.GetContextAsync();
if (ctx.Request.HttpMethod != "GET") {
closeResponseWith(ctx, 400, "GETs only");
return;
}
var target = Path.Combine(RootPath, ctx.Request.Url.AbsolutePath.Replace('/', Path.DirectorySeparatorChar).Substring(1));
var fi = new FileInfo(target);
if (!fi.FullName.StartsWith(RootPath)) {
closeResponseWith(ctx, 401, "Not authorized");
return;
}
if (!fi.Exists) {
closeResponseWith(ctx, 404, "Not found");
return;
}
try {
using (var input = File.OpenRead(target)) {
ctx.Response.StatusCode = 200;
input.CopyTo(ctx.Response.OutputStream);
ctx.Response.Close();
}
} catch (Exception ex) {
closeResponseWith(ctx, 500, ex.ToString());
}
}
});
var ret = Disposable.Create(() => {
shouldStop = true;
server.Stop();
listener.Wait(2000);
inner = null;
});
inner = ret;
return ret;
}
static void closeResponseWith(HttpListenerContext ctx, int statusCode, string message)
{
ctx.Response.StatusCode = statusCode;
using (var sw = new StreamWriter(ctx.Response.OutputStream, Encoding.UTF8)) {
sw.WriteLine(message);
}
ctx.Response.Close();
}
public void Dispose()
{
var toDispose = Interlocked.Exchange(ref inner, null);
if (toDispose != null) {
toDispose.Dispose();
}
}
}
}

94
test/UtilityTests.cs Normal file
View File

@@ -0,0 +1,94 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
using Splat;
using Squirrel;
using Squirrel.Tests.TestHelpers;
using Xunit;
namespace Squirrel.Tests.Core
{
public class UtilityTests : IEnableLogger
{
[Fact]
public void ShaCheckShouldBeCaseInsensitive()
{
var sha1FromExternalTool = "75255cfd229a1ed1447abe1104f5635e69975d30";
var inputPackage = IntegrationTestHelper.GetPath("fixtures", "Squirrel.Core.1.0.0.0.nupkg");
var stream = File.OpenRead(inputPackage);
var sha1 = Utility.CalculateStreamSHA1(stream);
Assert.NotEqual(sha1FromExternalTool, sha1);
Assert.Equal(sha1FromExternalTool, sha1, StringComparer.OrdinalIgnoreCase);
}
[Fact]
public void CanDeleteDeepRecursiveDirectoryStructure()
{
string tempDir;
using (Utility.WithTempDirectory(out tempDir)) {
for (var i = 0; i < 50; i++) {
var directory = Path.Combine(tempDir, newId());
CreateSampleDirectory(directory);
}
var files = Directory.GetFiles(tempDir, "*", SearchOption.AllDirectories);
var count = files.Count();
this.Log().Info("Created {0} files under directory {1}", count, tempDir);
var sw = new Stopwatch();
sw.Start();
Utility.DeleteDirectory(tempDir).Wait();
sw.Stop();
this.Log().Info("Delete took {0}ms", sw.ElapsedMilliseconds);
Assert.False(Directory.Exists(tempDir));
}
}
static void CreateSampleDirectory(string directory)
{
while (true) {
Directory.CreateDirectory(directory);
for (var j = 0; j < 100; j++) {
var file = Path.Combine(directory, newId());
if (file.Length > 260) continue;
File.WriteAllText(file, Guid.NewGuid().ToString());
}
if (new Random().NextDouble() > 0.5) {
var childDirectory = Path.Combine(directory, newId());
if (childDirectory.Length > 248) return;
directory = childDirectory;
continue;
}
break;
}
}
static string newId()
{
var text = Guid.NewGuid().ToString();
var bytes = Encoding.Unicode.GetBytes(text);
var provider = new SHA1Managed();
var hashString = string.Empty;
foreach (var x in provider.ComputeHash(bytes)) {
hashString += String.Format("{0:x2}", x);
}
if (hashString.Length > 7) {
return hashString.Substring(0, 7);
}
return hashString;
}
}
}

BIN
test/fixtures/Caliburn.Micro.1.4.1.nupkg vendored Normal file

Binary file not shown.

BIN
test/fixtures/Caliburn.Micro.1.5.2.nupkg vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

1
test/fixtures/RELEASES-OnePointOh vendored Normal file
View File

@@ -0,0 +1 @@
94689fede03fed7ab59c24337673a27837f0c3ec MyCoolApp-1.0.nupkg 1004502

3
test/fixtures/RELEASES-OnePointOne vendored Normal file
View File

@@ -0,0 +1,3 @@
94689fede03fed7ab59c24337673a27837f0c3ec MyCoolApp-1.0.nupkg 1004502
3a2eadd15dd984e4559f2b4d790ec8badaeb6a39 MyCoolApp-1.1.nupkg 1040561
14db31d2647c6d2284882a2e101924a9c409ee67 MyCoolApp-1.1-delta.nupkg 80396

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,29 @@
<?xml version="1.0"?>
<package xmlns="http://schemas.microsoft.com/packaging/2010/07/nuspec.xsd">
<metadata>
<version>1.1.0.0</version>
<authors>Administrator</authors>
<owners>Administrator</owners>
<dependencies>
<dependency id="DotNetZip" version="1.9.1.8" />
<dependency id="Ix_Experimental-Main" version="1.1.10823" />
<dependency id="NLog" version="2.0.0.2000" />
<dependency id="reactiveui-core" version="2.5.2.0" />
</dependencies>
<id>NSync.Core</id>
<requireLicenseAcceptance>false</requireLicenseAcceptance>
<description>Description</description>
<releaseNotes>
## Release Notes for 1.1
1. Did some cool stuff
1. Did another thing
1. Did a third thing, pretty crazy!
Make *sure* to refrobulate the confabulator or **terrible things will happen!**
</releaseNotes>
</metadata>
<files>
<file src="lib\net40\NSync.Core.dll" target="lib\net40\NSync.Core.dll" />
</files>
</package>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<Default Extension="nuspec" ContentType="application/octet" />
<Default Extension="exe" ContentType="application/octet" />
<Default Extension="psmdcp" ContentType="application/vnd.openxmlformats-package.core-properties+xml" />
<Default Extension="diff" ContentType="application/octet" />
<Default Extension="dll" ContentType="application/octet" />
<Default Extension="shasum" ContentType="text/plain" />
</Types>

7
test/fixtures/content-types/basic.xml vendored Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml" />
<Default Extension="nuspec" ContentType="application/octet" />
<Default Extension="exe" ContentType="application/octet" />
<Default Extension="psmdcp" ContentType="application/vnd.openxmlformats-package.core-properties+xml" />
</Types>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/>
<Default ContentType="application/octet" Extension="nuspec"/>
<Default ContentType="application/octet" Extension="html"/>
<Default ContentType="application/octet" Extension="nsi"/>
<Default ContentType="application/octet" Extension="css"/>
<Default ContentType="application/octet" Extension="otf"/>
<Default ContentType="application/octet" Extension="png"/>
<Default ContentType="application/octet" Extension="js"/>
<Default ContentType="application/octet" Extension="json"/>
<Default ContentType="application/octet" Extension="md"/>
<Default ContentType="application/octet" Extension="yml"/>
<Default ContentType="application/octet" Extension="eot"/>
<Default ContentType="application/octet" Extension="svg"/>
<Default ContentType="application/octet" Extension="ttf"/>
<Default ContentType="application/octet" Extension="woff"/>
<Default ContentType="application/octet" Extension="jpg"/>
<Default ContentType="application/octet" Extension="less"/>
<Default ContentType="application/octet" Extension="coffee"/>
<Default ContentType="application/octet" Extension="map"/>
<Default ContentType="application/octet" Extension="txt"/>
<Default ContentType="application/octet" Extension="gif"/>
<Default ContentType="application/octet" Extension="markdown"/>
<Default ContentType="application/octet" Extension="jade"/>
<Default ContentType="application/octet" Extension="sh"/>
<Default ContentType="application/octet" Extension="text"/>
<Default ContentType="application/octet" Extension="pl"/>
<Default ContentType="application/octet" Extension="dphpd"/>
<Default ContentType="application/octet" Extension="php"/>
<Default ContentType="application/octet" Extension="refine"/>
<Default ContentType="application/octet" Extension="wav"/>
<Default ContentType="application/octet" Extension="mdown"/>
<Default ContentType="application/octet" Extension="bar"/>
<Default ContentType="application/octet" Extension="template"/>
<Default ContentType="application/octet" Extension="ico"/>
<Default ContentType="application/octet" Extension="dll"/>
<Default ContentType="application/octet" Extension="pak"/>
<Default ContentType="application/octet" Extension="exe"/>
<Default ContentType="application/octet" Extension="diff"/>
<Default ContentType="text/plain" Extension="shasum"/>
<Default ContentType="application/vnd.openxmlformats-package.core-properties+xml" Extension="psmdcp"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/bootstrap/CNAME"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/bootstrap/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/hammerjs/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/iscroll/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/jqBootstrapValidation/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/jquery-ui/MANIFEST"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/page/Makefile"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/bar"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/foo"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/doh/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/doh/README"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/doh/_sounds/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/toUrl/sub/noext"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs-domready/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs-plugins/examples/js/foo"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs-text/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/select2/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/underscore/CNAME"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/underscore/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/underscore/Rakefile"/>
</Types>

62
test/fixtures/content-types/complex.xml vendored Normal file
View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types">
<Default ContentType="application/vnd.openxmlformats-package.relationships+xml" Extension="rels"/>
<Default ContentType="application/octet" Extension="nuspec"/>
<Default ContentType="application/octet" Extension="html"/>
<Default ContentType="application/octet" Extension="nsi"/>
<Default ContentType="application/octet" Extension="css"/>
<Default ContentType="application/octet" Extension="otf"/>
<Default ContentType="application/octet" Extension="png"/>
<Default ContentType="application/octet" Extension="js"/>
<Default ContentType="application/octet" Extension="json"/>
<Default ContentType="application/octet" Extension="md"/>
<Default ContentType="application/octet" Extension="yml"/>
<Default ContentType="application/octet" Extension="eot"/>
<Default ContentType="application/octet" Extension="svg"/>
<Default ContentType="application/octet" Extension="ttf"/>
<Default ContentType="application/octet" Extension="woff"/>
<Default ContentType="application/octet" Extension="jpg"/>
<Default ContentType="application/octet" Extension="less"/>
<Default ContentType="application/octet" Extension="coffee"/>
<Default ContentType="application/octet" Extension="map"/>
<Default ContentType="application/octet" Extension="txt"/>
<Default ContentType="application/octet" Extension="gif"/>
<Default ContentType="application/octet" Extension="markdown"/>
<Default ContentType="application/octet" Extension="jade"/>
<Default ContentType="application/octet" Extension="sh"/>
<Default ContentType="application/octet" Extension="text"/>
<Default ContentType="application/octet" Extension="pl"/>
<Default ContentType="application/octet" Extension="dphpd"/>
<Default ContentType="application/octet" Extension="php"/>
<Default ContentType="application/octet" Extension="refine"/>
<Default ContentType="application/octet" Extension="wav"/>
<Default ContentType="application/octet" Extension="mdown"/>
<Default ContentType="application/octet" Extension="bar"/>
<Default ContentType="application/octet" Extension="template"/>
<Default ContentType="application/octet" Extension="ico"/>
<Default ContentType="application/octet" Extension="dll"/>
<Default ContentType="application/octet" Extension="pak"/>
<Default ContentType="application/octet" Extension="exe"/>
<Default ContentType="application/vnd.openxmlformats-package.core-properties+xml" Extension="psmdcp"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/bootstrap/CNAME"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/bootstrap/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/hammerjs/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/iscroll/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/jqBootstrapValidation/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/jquery-ui/MANIFEST"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/page/Makefile"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/bar"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/foo"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/doh/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/doh/README"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/doh/_sounds/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs/tests/toUrl/sub/noext"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs-domready/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs-plugins/examples/js/foo"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/requirejs-text/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/select2/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/underscore/CNAME"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/underscore/LICENSE"/>
<Override ContentType="application/octet" PartName="/Content/frontend/js/library/underscore/Rakefile"/>
</Types>

View File

@@ -0,0 +1,57 @@
Apache License, Version 2.0
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
1. You must give any other recipients of the Work or Derivative Works a copy of this License; and
2. You must cause any modified files to carry prominent notices stating that You changed the files; and
3. You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
4. If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS

View File

@@ -0,0 +1,71 @@
=== version 3.0
================================================================================================
change - Removed overloads of logging methods that were taking format string from ILogger and
ILogger and IExtendedLogger and didn't have word Format in their name.
For example:
void Error(string format, params object[] args); // was removed
void ErrorFormat(string format, params object[] args); //use this one instead
impact - low
fixability - medium
revision -
description - To minimize confusion and duplication those methods were removed.
fix - Use methods that have explicit "Format" word in their name and same signature.
================================================================================================
change - Removed WebLogger and WebLoggerFactory
impact - low
fixability - medium
revision -
description - To minimize management overhead the classes were removed so that only single
Client Profile version of Castle.Core can be distributed.
fix - You can use NLog or Log4Net web logger integration, or reuse implementation of existing
web logger and use it as a custom logger.
================================================================================================
change - Removed obsolete overload of ProxyGenerator.CreateClassProxy
impact - low
fixability - trivial
revision -
description - Deprecated overload of ProxyGenerator.CreateClassProxy was removed to keep the
method consistent with other methods and to remove confusion
fix - whenever removed overload was used, use one of the other overloads.
================================================================================================
change - IProxyGenerationHook.NonVirtualMemberNotification method was renamed
impact - high
fixability - easy
revision -
description - to accommodate class proxies with target method NonVirtualMemberNotification on
IProxyGenerationHook type was renamed to more accurate NonProxyableMemberNotification
since for class proxies with target not just methods but also fields and other member that
break the abstraction will be passed to this method.
fix - whenever NonVirtualMemberNotification is used/implemented change the method name to
NonProxyableMemberNotification. Implementors should also accommodate possibility that not
only MethodInfos will be passed as method's second parameter.
================================================================================================
change - DynamicProxy will now allow to intercept members of System.Object
impact - very low
fixability - easy
revision -
description - to allow scenarios like mocking of System.Object members, DynamicProxy will not
disallow proxying of these methods anymore. AllMethodsHook (default IProxyGenerationHook)
will still filter them out though.
fix - whenever custom IProxyGenerationHook is used, user should account for System.Object's
members being now passed to ShouldInterceptMethod and NonVirtualMemberNotification methods
and if neccessary update the code to handle them appropriately.

Binary file not shown.

View File

@@ -0,0 +1,196 @@
3.2.0 (2013-02-16)
==================
- fixed DYNPROXY-179 - Exception when creating a generic proxy (from cache)
- fixed DYNPROXY-175 - invalid CompositionInvocation type used when code uses interface proxies with and without InterceptorSelector
3.1.0 (2012-08-05)
==================
- fixed DYNPROXY-174 - Unable to cast object of type 'System.Collections.ObjectModel.ReadOnlyCollection`1[System.Reflection.CustomAttributeTypedArgument]' to type 'System.Array'
3.1.0 RC (2012-07-08)
==================
- support multiple inheritance of DA attributes on interfaces.
- BREAKING CHANGE: removed propogate child notifications as it violated INotifyPropertyChanged contract
- improved DictionaryAdapter performance
- generalized IBindingList support for DictionaryAdapters
- added reference support to XmlAdapter
- BRAKING CHANGE: refactored XPathAdapter into XmlAdapter with much more flexibility to support other input like XLinq
- implemented CORE-43 - Add option to skip configuring log4net/nlog
- fixed CORE-44 - NLog logger does not preserver call site info
- fixed DYNPROXY-171 - PEVerify error on generic method definition
- fixed DYNPROXY-170 - Calls to properties inside non-intercepted methods are not forwarded to target object (regression from v2.5)
- fixed DYNPROXY-169 - Support IChangeProxyTarget on additional interfaces and mixins when using CreateInterfaceProxyWithTargetInterface
3.0.0 (2011-12-13)
==================
no major changes since RC
3.0.0 RC 1 (2011-11-20)
==================
- Applied Jeff Sharps patch that refactored Xml DictionaryAdapter to improve maintainability and enable more complete functionality
- fixed DYNPROXY-165 - Object.GetType() and Object.MemberwiseClone() should be ignored and not reported as non-interceptable to IProxyGenerationHook
- fixed DYNPROXY-164 - Invalid Proxy type generated when there are more than one base class generic constraints
- fixed DYNPROXY-162 - ref or out parameters can not be passed back if proxied method throw an exception
3.0.0 beta 1 (2011-08-14)
==================
- fixed CORE-37 - TAB characters in the XML Configuration of a component parameter is read as String.Empty
- fixed DYNPROXY-161 - Strong Named DynamicProxy Assembly Not Available in Silverligh
- fixed DYNPROXY-159 - Sorting MemberInfo array for serialization has side effects
- fixed DYNPROXY-158 - Can't create class proxy with target and without target in same ProxyGenerator
- fixed DYNPROXY-153 - When proxying a generic interface which has an interface as GenericType . No proxy can be created
- fixed DYNPROXY-151 - Cast error when using attributes
- implemented CORE-33 - Add lazy logging
- implemented DYNPROXY-156 - Provide mechanism for interceptors to implement retry logic
- removed obsolete members from ILogger and its implementations
2.5.2 (2010-11-15)
==================
- fixed DYNPROXY-150 - Finalizer should not be proxied
- implemented DYNPROXY-149 - Make AllMethodsHook members virtual so it can be used as a base class
- fixed DYNPROXY-147 - Can't crete class proxies with two non-public methods having same argument types but different return type
- fixed DYNPROXY-145 Unable to proxy System.Threading.SynchronizationContext (.NET 4.0)
- fixed DYNPROXY-144 - params argument not supported in constructor
- fixed DYNPROXY-143 - Permit call to reach "non-proxied" methods of inherited interfaces
- implemented DYNPROXY-139 - Better error message
- fixed DYNPROXY-133 - Debug assertion in ClassProxyInstanceContributor fails when proxying ISerializable with an explicit implementation of GetObjectData
- fixed CORE-32 - Determining if permission is granted via PermissionUtil does not work in .NET 4
- applied patch by Alwin Meijs - ExtendedLog4netFactory can be configured with a stream from for example an embedded log4net xml config
- Upgraded NLog to 2.0 Beta 1
- Added DefaultXmlSerializer to bridge XPathAdapter with standard Xml Serialization.
- XPathAdapter for DictionaryAdapter added IXPathSerializer to provide hooks for custom serialization.
2.5.1 (2010-09-21)
==================
- Interface proxy with target Interface now accepts null as a valid target value (which can be replaced at a later stage).
- DictionaryAdapter behavior overrides are now ordered with all other behaviors
- BREAKING CHANGE: removed web logger so that by default Castle.Core works in .NET 4 client profile
- added paramter to ModuleScope disabling usage of signed modules. This is to workaround issue DYNPROXY-134. Also a descriptive exception message is being thrown now when the issue is detected.
- Added IDictionaryBehaviorBuilder to allow grouping behaviors
- Added GenericDictionaryAdapter to simplify generic value sources
- fixed issue DYNPROXY-138 - Error message missing space
- fixed false positive where DynamicProxy would not let you proxy interface with target interface when target object was a COM object.
- fixed ReflectionBasedDictionaryAdapter when using indexed properties
2.5.0 (2010-08-21)
==================
- DynamicProxy will now not replicate non-public attribute types
- Applied patch from Kenneth Siewers M<>ller which adds parameterless constructor to DefaultSmtpSender implementation, to be able to configure the inner SmtpClient from the application configuration file (system.net.smtp).
- added support for .NET 4 and Silverlight 4, updated solution to VisualStudio 2010
- Removed obsolete overload of CreateClassProxy
- Added class proxy with taget
- Added ability to intercept explicitly implemented generic interface methods on class proxy.
- DynamicProxy does not disallow intercepting members of System.Object anymore. AllMethodsHook will still filter them out though.
- Added ability to intercept explicitly implemented interface members on class proxy. Does not support generic members.
- Merged DynamicProxy into Core binary
- fixed DYNPROXY-ISSUE-132 - "MetaProperty equals implementation incorrect"
- Fixed bug in DiagnosticsLoggerTestCase, where when running as non-admin, the teardown will throw SecurityException (contributed by maxild)
- Split IoC specific classes into Castle.Windsor project
- Merged logging services solution
- Merged DynamicProxy project
1.2.0 (2010-01-11)
==================
- Added IEmailSender interface and its default implementation
1.2.0 beta (2009-12-04)
==================
- BREAKING CHANGE - added ChangeProxyTarget method to IChangeProxyTarget interface
- added docs to IChangeProxyTarget methods
- Fixed DYNPROXY-ISSUE-108 - Obtaining replicated custom attributes on proxy may fail when property setter throws exception on default value
- Moved custom attribute replication from CustomAttributeUtil to new interface - IAttributeDisassembler
- Exposed IAttributeDisassembler via ProxyGenerationOptions, so that users can plug their implementation for some convoluted scenarios. (for Silverlight)
- Moved IInterceptorSelector from Dynamic Proxy to Core (IOC-ISSUE-156)
1.1.0 (2009-05-04)
==================
- Applied Eric Hauser's patch fixing CORE-ISSUE-22
"Support for environment variables in resource URI"
- Applied Gauthier Segay's patch fixing CORE-ISSUE-20
"Castle.Core.Tests won't build via nant because it use TraceContext without referencing System.Web.dll"
- Added simple interface to ComponentModel to make optional properties required.
- Applied Mark's -- <mwatts42@gmail.com> -- patch that changes
the Core to support being compiled for Silverlight 2
- Applied Louis DeJardin's patch adding TraceLogger as a new logger implementation
- Applied Chris Bilson's patch fixing CORE-15
"WebLogger Throws When Logging Outside of an HttpContext"
Release Candidate 3
===================
- Added IServiceProviderEx which extends IServiceProvider
- Added Pair<T,S> class.
- Applied Bill Pierce's patch fixing CORE-9
"Allow CastleComponent Attribute to Specify Lifestyle in Constructor"
- Added UseSingleInterfaceProxy to CompomentModel to control the proxying
behavior while maintaining backward compatibility.
Added the corresponding ComponentProxyBehaviorAttribute.
- Made NullLogger and IExtnededLogger
- Enabled a new format on ILogger interface, with 6 overloads for each method:
Debug(string)
Debug(string, Exception)
Debug(string, params object[])
DebugFormat(string, params object[])
DebugFormat(Exception, string, params object[])
DebugFormat(IFormatProvider, string, params object[])
DebugFormat(IFormatProvider, Exception, string, params object[])
The "FatalError" overloads where marked as [Obsolete], replaced by "Fatal" and "FatalFormat".
0.0.1.0
=======
- Included IProxyTargetAccessor
- Removed IMethodInterceptor and IMethodInvocation, that have been replaced
by IInterceptor and IInvocation
- Added FindByPropertyInfo to PropertySetCollection
- Made the DependencyModel.IsOptional property writable
- Applied Curtis Schlak's patch fixing IOC-27
"assembly resource format only works for resources where the assemblies name and default namespace are the same."
Quoting:
"I chose to preserve backwards compatibility by implementing the code in the
reverse order as suggested by the reporter. Given the following URI for a resource:
assembly://my.cool.assembly/context/moo/file.xml
It will initially look for an embedded resource with the manifest name of
"my.cool.assembly.context.moo.file.xml" in the loaded assembly my.cool.assembly.dll.
If it does not find it, then it looks for the embedded resource with the manifest name
of "context.moo.file.xml".
- IServiceEnabledComponent Introduced to be used across the project as
a standard way to have access to common services, for example, logger factories
- Added missing log factories
- Refactor StreamLogger and DiagnosticLogger to be more consistent behavior-wise
- Refactored WebLogger to extend LevelFilteredLogger (removed duplication)
- Refactored LoggerLevel order
- Project started

View File

@@ -0,0 +1,13 @@
Copyright 2004-2013 Castle Project - http://www.castleproject.org/
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

Binary file not shown.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,10 @@
Thanks for downloading this Castle package.
You can find full list of changes in changes.txt
Documentation (work in progress, contributions appreciated):
Dictionary Adapter - http://docs.castleproject.org/Tools.Castle-DictionaryAdapter.ashx
DynamicProxy - http://docs.castleproject.org/Tools.DynamicProxy.ashx
Discusssion group: - http://groups.google.com/group/castle-project-users
StackOverflow tags: - castle-dynamicproxy, castle-dictionaryadapter, castle
Issue tracker: - http://issues.castleproject.org/dashboard

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,93 @@
param($installPath, $toolsPath, $package, $project)
# open json.net splash page on package install
# don't open if json.net is installed as a dependency
try
{
$url = "http://james.newtonking.com/json"
$dte2 = Get-Interface $dte ([EnvDTE80.DTE2])
if ($dte2.ActiveWindow.Caption -eq "Package Manager Console")
{
# user is installing from VS NuGet console
# get reference to the window, the console host and the input history
# show webpage if "install-package newtonsoft.json" was last input
$consoleWindow = $(Get-VSComponentModel).GetService([NuGetConsole.IPowerConsoleWindow])
$props = $consoleWindow.GetType().GetProperties([System.Reflection.BindingFlags]::Instance -bor `
[System.Reflection.BindingFlags]::NonPublic)
$prop = $props | ? { $_.Name -eq "ActiveHostInfo" } | select -first 1
if ($prop -eq $null) { return }
$hostInfo = $prop.GetValue($consoleWindow)
if ($hostInfo -eq $null) { return }
$history = $hostInfo.WpfConsole.InputHistory.History
$lastCommand = $history | select -last 1
if ($lastCommand)
{
$lastCommand = $lastCommand.Trim().ToLower()
if ($lastCommand.StartsWith("install-package") -and $lastCommand.Contains("newtonsoft.json"))
{
$dte2.ItemOperations.Navigate($url) | Out-Null
}
}
}
else
{
# user is installing from VS NuGet dialog
# get reference to the window, then smart output console provider
# show webpage if messages in buffered console contains "installing...newtonsoft.json" in last operation
$instanceField = [NuGet.Dialog.PackageManagerWindow].GetField("CurrentInstance", [System.Reflection.BindingFlags]::Static -bor `
[System.Reflection.BindingFlags]::NonPublic)
$consoleField = [NuGet.Dialog.PackageManagerWindow].GetField("_smartOutputConsoleProvider", [System.Reflection.BindingFlags]::Instance -bor `
[System.Reflection.BindingFlags]::NonPublic)
if ($instanceField -eq $null -or $consoleField -eq $null) { return }
$instance = $instanceField.GetValue($null)
if ($instance -eq $null) { return }
$consoleProvider = $consoleField.GetValue($instance)
if ($consoleProvider -eq $null) { return }
$console = $consoleProvider.CreateOutputConsole($false)
$messagesField = $console.GetType().GetField("_messages", [System.Reflection.BindingFlags]::Instance -bor `
[System.Reflection.BindingFlags]::NonPublic)
if ($messagesField -eq $null) { return }
$messages = $messagesField.GetValue($console)
if ($messages -eq $null) { return }
$operations = $messages -split "=============================="
$lastOperation = $operations | select -last 1
if ($lastOperation)
{
$lastOperation = $lastOperation.ToLower()
$lines = $lastOperation -split "`r`n"
$installMatch = $lines | ? { $_.StartsWith("------- installing...newtonsoft.json ") } | select -first 1
if ($installMatch)
{
$dte2.ItemOperations.Navigate($url) | Out-Null
}
}
}
}
catch
{
# stop potential errors from bubbling up
# worst case the splash page won't open
}
# yolo

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More