25

We use Hudson to build our projects, and Hudson conveniently defines environment variables like %BUILD_NUMBER% at compile time.

I'd like to use that variable in code, so we can do things like log what build this is at run time. However I cannot do System.Environment.GetEnvironmentVariable because that is accessing the run-time environment, what I want is something like:

#define BUILD_NUM = %BUILD_NUMBER%

Or:

const string BUILD_NUM = %BUILD_NUMBER%

Except I don't know the syntax. Can someone please point me in the right direction? Thanks!

user16217248
  • 3,119
  • 19
  • 19
  • 37
Eggplant Jeff
  • 1,749
  • 2
  • 15
  • 20
  • You could use pre-build action/macro to change the number, would that be an option? – Bobby Dec 15 '10 at 13:16
  • 1
    I was hoping for something that didn't require modifying the files every time, where the input source files remain unchanged and the current value is inserted into the compiled output. – Eggplant Jeff Dec 15 '10 at 14:59

5 Answers5

23

Okay here's what I wound up doing. It's not very elegant, but it works. I created a pre-build step that looks like this:

echo namespace Some.Namespace > "$(ProjectDir)\CiInfo.cs"
echo { >> "$(ProjectDir)\CiInfo.cs"
echo     ///^<summary^>Info about the continuous integration server build that produced this binary.^</summary^> >> "$(ProjectDir)\CiInfo.cs"
echo     public static class CiInfo >> "$(ProjectDir)\CiInfo.cs"
echo     { >> "$(ProjectDir)\CiInfo.cs"
echo         ///^<summary^>The current build number, such as "153"^</summary^> >> "$(ProjectDir)\CiInfo.cs"
echo         public const string BuildNumber = ("%BUILD_NUMBER%" == "" ? @"Unknown" : "%BUILD_NUMBER%"); >> "$(ProjectDir)\CiInfo.cs"
echo         ///^<summary^>String of the build number and build date/time, and other useful info.^</summary^> >> "$(ProjectDir)\CiInfo.cs"
echo         public const string BuildTag = ("%BUILD_TAG%" == "" ? @"nohudson" : "%BUILD_TAG%") + " built: %DATE%-%TIME%"; >> "$(ProjectDir)\CiInfo.cs"
echo     } >> "$(ProjectDir)\CiInfo.cs"
echo } >> "$(ProjectDir)\CiInfo.cs"

Then I added "CiInfo.cs" to the project, but ignored it from version control. That way I never have to edit it or commit it, and the project always has a constant available that is the latest build number and time.

Eggplant Jeff
  • 1,749
  • 2
  • 15
  • 20
  • Thank you. I would make it a batch file and then call it from the batch and pass in the file name with the path and use %1 for the redirect that may be easier to maintain if most of it can be handled in batch: `$(ProjectDir)\Tools\BuildCI.bat "$(ProjectDir)\CiInfo.cs"` – Charles Byrne Sep 10 '14 at 20:36
  • I love this. So simple, no plugins! Thanks. – arkod Feb 26 '16 at 12:50
  • I was looking for this question title, because I wanted to do exactly the same thing as this answer is illustrating. +1 – Zuu Apr 07 '16 at 09:38
  • This made me smile with it's ingenuity. Thanks. If you're parsing the %DATE% value, definitely use, say `CultureInfo("en-AU")` (or similar) to ensure that "17/12/2017" doesn't blow things up for the Americans. It's possibly worth adding a comment at the top of the Class something along the usual lines of, "This code was created by a tool ... changes will be lost ...", etc in case someone's wondering down the track.. – SteveCinq Dec 18 '17 at 04:25
  • For those looking for a more customizable date, you can try something like `echo public const string BuildTimestamp = "$([System.DateTime]::UtcNow.ToString("yyyy-MM-ddTHH:mm:ss 'GMT'"))"; >> "$(ProjectDir)\CiInfo.cs"` – Bob_Gneu Jan 15 '19 at 05:51
7

One way to do it is to add a build-step before compilation which does a regex replace in the appropriate source file(s) for %BUILD_NUMBER%.

Mark Pim
  • 9,898
  • 7
  • 40
  • 59
  • +1, although in the past when I have done it, I've simply targeted the `.config` file as a setting and then the rest of the code just accesses it from there. – Moo-Juice Dec 15 '10 at 13:45
  • This is roughly what we currently do (against config files though, not source files), I was hoping I could just access it directly in the code though because it's considerably simpler, less error prone, and won't show as a change vs. the svn repository. – Eggplant Jeff Dec 15 '10 at 14:52
6

One possibility is to use T4 to generate your configuration class with all the constants instantiated. T4 is well-integrated into MSVS, no need for your own custom build step.

SK-logic
  • 9,605
  • 1
  • 23
  • 35
  • Thanks a lot! I develop with Visual Studio for like 20 years now and never new such a thing even existed :) – Drolevar Feb 17 '21 at 12:04
1

define does not allow you to define contants in C# like you can in C/C++.

From this page:

The #define directive cannot be used to declare constant values as is typically done in C and C++. Constants in C# are best defined as static members of a class or struct. If you have several such constants, consider creating a separate "Constants" class to hold them.

If you are looking to reflect the build number in you AssemblyInfo class, most build tools support generating that class at build time. MSBuild has a task for it. As does NAnt. Not sure how Hudson does this.

dkackman
  • 15,179
  • 13
  • 69
  • 123
1

I had a similar problem.

I was developing a Xamarin mobile app with an ASP.Net backend. I had a settings class that contains the backend server URL:

namespace Company.Mobile
{
    public static class Settings
    {
#if DEBUG
        const string WebApplicationBaseUrl = "https://local-pc:44335/";
#else
        const string WebApplicationBaseUrl = "https://company.com/";
#endif
    }
}

It has different values for debug and release configurations. But this didn't work when several developers started working on the project. Every dev machine had its IP address, and mobile phones need to connect using unique IP addresses.

I needed to set the constant value from a file or an environment variable on each dev machine. This is where Fody fits in. I used it to create an in solution weaver. Here are the details.

I place my Settings class in the Xamarin app project. This project has to include the Fody Nuget package:

<ItemGroup>
    <PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Fody" Version="6.2.0">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
    <WeaverFiles Include="$(SolutionDir)Company.Mobile.Models\bin\Debug\netstandard2.0\Company.Mobile.Models.dll" WeaverClassNames="SetDevServerUrlWeaver" />
  </ItemGroup>

I make my setup work on Debug configuration only, because I don't want the substitution to happen on Release builds.

The weaver class is placed in a class library project (Company.Mobile.Models) that the mobile project depends on (you needn't and shouldn't have this dependency, but Fody docs says clearly that the project that contains the weaver must be built before the project that emits the weaved assembly). This library project includes the FodyHelpers Nuget package:

<ItemGroup Condition="'$(Configuration)' == 'Debug'">
        <PackageReference Include="FodyHelpers" Version="6.2.0" />
    </ItemGroup>

The weaver class is defined as follows:

#if DEBUG

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;

using Fody;

namespace Company.Mobile.Models
{
    public class SetDevServerUrlWeaver : BaseModuleWeaver
    {
        private const string SettingsClassName = "Settings",
            DevServerUrlFieldName = "WebApplicationBaseUrl",
            DevServerUrlSettingFileName = "devServerUrl.txt";

        public override void Execute()
        {
            var target = this.ModuleDefinition.Types.SingleOrDefault(t => t.IsClass && t.Name == SettingsClassName);

            var targetField = target.Fields.Single(f => f.Name == DevServerUrlFieldName);

            try
            {
                targetField.Constant = File.ReadAllText(Path.Combine(this.ProjectDirectoryPath, DevServerUrlSettingFileName));
            }
            catch
            {
                this.WriteError($"Place a file named {DevServerUrlSettingFileName} and place in it the dev server URL");

                throw;
            }
        }

        public override IEnumerable<string> GetAssembliesForScanning()
        {
            yield return "Company.Mobile";
        }
    }
}

#endif

And here's the FodyWeavers.xml file placed in the Mobile app project:

<?xml version="1.0" encoding="utf-8"?>
<Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
  <SetDevServerUrlWeaver />
</Weavers>

The devServerUrl.txt simply contains my local IP: https://192.168.1.111:44335/. This file must not be added to source control. Add it to your source control ignore file so that each developer have his version.

You may easily read the substituted value from an environment variable (System.Environment.GetEnvironmentVariable) or whatever place instead of a file.

I hoped there had been a better way to do this, like Roslyn, or this attribute that seems to do the job, but it doesn't.

Ashraf Sabry
  • 3,081
  • 3
  • 32
  • 29