Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>In the end, I decided to just roll my own C# console application to automate my entire site build process. As these things always do, it took much longer to put together than I would have liked, but now one command takes my site straight from Subversion to production, so I'm very happy.</p> <p>First off, I used the fantastic <a href="http://www.ndesk.org/Options" rel="nofollow noreferrer">Mono.Options</a> class to handle grabbing command line arguments. It's <a href="http://anonsvn.mono-project.com/viewvc/trunk/mcs/class/Mono.Options/Mono.Options/Options.cs" rel="nofollow noreferrer">a single .cs file</a> you can just add to your project and be good to go.</p> <p>I wanted command line arguments so&mdash;for example&mdash;I could specify which revision to deploy (if I didn't want the HEAD).</p> <pre><code>using Mono.Options; int rev = 0; OptionSet opt = new OptionSet(); opt.Add("r=|revison=", "Revision to deploy (defaults to HEAD).", v =&gt; rev = int.Parse(v)); </code></pre> <p>Once you have your options all set up, you can even do <code>opt.WriteOptionDescriptions(Console.Out);</code> to print out a usage help message.</p> <p>I grabbed <a href="http://sharpsvn.open.collab.net/" rel="nofollow noreferrer">SharpSvn</a> to handle the svn export; it was actually much easier than expected to implement.</p> <pre><code>using SharpSvn; SvnClient svn = new SvnClient(); svn.Authentication.DefaultCredentials = new System.Net.NetworkCredential("account", "password"); // Since this is an internal-only tool, I'm not too worried about just // hardcoding the credentials of an account with read-only access. SvnExportArgs arg = new SvnExportArgs(); arg.Revision = rev &gt; 0 ? new SvnRevision(rev) : new SvnRevision(SvnRevisionType.Head); svn.Export(new SvnUriTarget("&lt;repository URL&gt;"), workDir, arg); </code></pre> <p>...and the entire site is exported to a temp folder (<code>workDir</code>). Since I also wanted to print the svn revision to the site, I grabbed the current repository revision (if a revision wasn't specified).</p> <pre><code>SvnInfoEventArgs ifo; svn.GetInfo(new SvnUriTarget("&lt;repo URL&gt;"), out ifo); </code></pre> <p>Now <code>ifo.Revision</code> will have the HEAD revision.</p> <p>Since I had a small set of known include files, I decided to just load them into memory once, merge in the revision number where needed, and then do a simple <code>string.Replace</code> on each *.html file in the temp folder.</p> <pre><code>string[] files = Directory.GetFiles(workDir, "*.html", SearchOption.AllDirectories); foreach (string ff in files) { File.Move(ff, workDir + "_.tmp"); using (StreamReader reader = new StreamReader(workDir + "_.tmp")) { using (StreamWriter writer = new StreamWriter(ff)) { string line; while ((line = reader.ReadLine()) != null) { line = line.Replace("&lt;!--#include virtual=\"/top.html\" --&gt;", top); // &lt;etc..&gt; writer.WriteLine(line); } } } File.Delete(workDir + "_.tmp"); } </code></pre> <p>Move the unprocessed file to a temporary location, open a <code>StreamWriter</code> on the original file, read in the temp file, replace the known <code>&lt;!--#include--&gt;</code>s, and delete the temp file. This process completes in under a second.</p> <p>One of the other things I do is minify all my scripts and compile them into a single .js file. This allows me to keep things manageable in development (classes are logically organized into files), but optimize everything for production. (Since having twenty <code>&lt;script src="..."&gt;</code> tags is <a href="http://code.google.com/speed/articles/include-scripts-properly.html" rel="nofollow noreferrer">Very Bad</a>.)</p> <p>The <a href="http://htmlagilitypack.codeplex.com/" rel="nofollow noreferrer">HTML Agility Pack</a> was very useful for this task. I simply loaded my page template into a <code>HtmlDocument</code> and extracted the locations of the scripts which need to be minified and combined into a single file. (The remainder of the *.js files in my scripts directory only get loaded into certain pages, so I did not want them combined into the master file.)</p> <pre><code>using HtmlAgilityPack; HtmlDocument doc = new HtmlDocument(); doc.LoadHtml(top); using (StreamWriter writer = new StreamWriter(workDir + "js\\compiled.js")) { foreach (HtmlNode script in doc.DocumentNode.SelectNodes("//script")) { string js = script.Attributes["src"].Value; script.Remove(); js = js.Replace("/js/", workDir + "js/"); // In my site, all scripts are located in the /js folder. js = js.Replace("/", "\\"); string mini; if (js.IndexOf(".min.") &gt; 0) // It's already minified. { mini = js; } else { mini = workDir + "_.tmp"; MinifyScript(js, mini); } using (StreamReader sr = new StreamReader(mini)) writer.WriteLine(sr.ReadToEnd()); File.Delete(js); File.Delete(workDir + "_.tmp"); } } </code></pre> <p>Then look for the remaining scripts to minify:</p> <pre><code>string[] jsfolder = Directory.GetFiles(workDir + "js\\", "*.js"); foreach (string js in jsfolder) { if (js.IndexOf("\\compiled.js") &gt; 0) continue; // The compiled js file from above will be in the folder; we want to ignore it. MinifyScript(js, js); } </code></pre> <p>For the actual minification, I just used the <a href="http://developer.yahoo.com/yui/compressor/" rel="nofollow noreferrer">YUI Compressor</a>, which is a Java jar. You could substitute your compressor of choice here.</p> <pre><code>static void MinifyScript(string input, string output) { System.Diagnostics.ProcessStartInfo si = new System.Diagnostics.ProcessStartInfo(@"C:\Program Files (x86)\Java\jre6\bin\java.exe", "-jar mini.jar -o " + output + " " + input); si.RedirectStandardOutput = true; si.UseShellExecute = false; System.Diagnostics.Process proc = System.Diagnostics.Process.Start(si); proc.WaitForExit(); if (proc.ExitCode != 0) throw new Exception("Error compiling " + input + "."); } </code></pre> <p>In my build process, the minification step actually happens before the processing of the includes (since I reduce the number of <code>&lt;script&gt;</code> tags down to one in the template).</p> <p>Finally, I use <code>Microsoft.Web.Administration.ServerManager</code> to temporarily stop IIS while I move all of the files in my temp folder into the actual prouction site folder. (I wanted to prevent any weirdness while the site is in a half-deployed state.)</p> <pre><code>using Microsoft.Web.Administration; // Assembly is found in %windir%\System32\inetsrv\Microsoft.Web.Administration.dll ServerManager iis = new ServerManager(); if (stopIIS) iis.Sites[site].Stop(); // bool stopIIS and string site are set by command line option (and have hardcoded defaults). string[] files = Directory.GetFiles(workDir, "*"); foreach (string file in files) { string name = file.Substring(file.LastIndexOf("\\") + 1); if (name == "web.config") continue; // The web.config for production is different from that used in development and kept in svn. try { File.Delete(dest + name); // string dest is a command line option (and has a hard-coded default). } catch (Exception ex) { } File.Move(file, dest + name); } string[] dirs = Directory.GetDirectories(workDir); foreach (string dir in dirs) { string name = dir.Substring(dir.LastIndexOf("\\") + 1); if (name == "dyn") continue; // A folder I want to ignore. try { Directory.Delete(dest + name, true); } catch (DirectoryNotFoundException ex) { } Directory.Move(dir, dest + name); } if (stopIIS) iis.Sites[site].Start(); </code></pre> <p>And we're done. Phew!</p> <p>There’s a few details I omitted from the code above—for example, I delete all of the *.psd files in my images directory and write a copyright message to my compiled js file—but filling in the blanks is half the fun, right!?</p> <p>Obviously, some of the code I’ve presented here is applicable only to the specific design decisions I’ve made for my site, but I hope that some of you will find [portions] of this helpful if you decide to build an automated deploy process – which, for the record, I highly recommend. It’s very nice to be able to commit my changes to svn, ssh to my production server, run this, and be done.</p>
    singulars
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload