MSBuild started it. Where it ended was with an idea that I am constantly being reminded of: nothing is ever unchangeable.
Perhaps I should explain...
I have a new project I am working on for my company. This project is nothing terribly complicated, but part of our process is to build a CI/CD system for all of our projects using Azure DevOps. It's part of my job description to handle such things.
Lucky for me, an extraordinarily similar project already had a build and release definition in our CI/CD system, so my job was going to be super easy. If you've read my blog before, you know that merely thinking this is a bad idea.
The structure of this project was pretty straightforward. We had the following projects:
- An Enums project which contains all of our generated Enum values.
- An Impl project which contains all of our business-layer code.
- A Main project which is an ASP.NET 4.7.2 MVC application AND
- A UnitTests project which contains all of our tests.
I copied a Visual Studio Build task (using Visual Studio 2019) from the other, similar release definition in our Azure DevOps Pipelines. Said build task had the following parameters:
/p:OutDir=$(build.artifactstagingdirectory)\WebsiteName /p:publishUrl=$(build.artifactstagingdirectory)\WebsiteName /p:DeployOnBuild=true /p:DeployDefaultTarget=WebPublish /p:WebPublishMethod=FileSystem /p:DeleteExistingFiles=true /p:MvcBuildViews=true /p:MarkWebConfigAssistFilesAsExclude=false /p:TransformWebConfigEnabled=False
This looked, to me, like this was a perfectly valid MSBuild parameter set. Some of the parameters I recognized (MvcBuildViews), some had a meaning easily understood from their name (DeleteExistingFiles), and some were just there and I had to hope they worked as I thought they did.
All of that seemed fine and dandy, except when I actually ran the build in DevOps I kept getting the following errors:
The errors read, "Error CS0006: Metadata file 'path/to/file' could not be found". Which is not an error I'd ever seen before.
See, the UnitTests project relies upon the Enums and Impl projects to build, and it seemed as though when the UnitTests project was building, it could not find its dependencies. Which was especially strange because it worked just fine on my local machine.
I spent two days googling this error, trying tiny fixes in my solution, messing with the DevOps build pipeline. No matter what I tried, no matter what configurations I put into the project, nothing seemed to work.
Desperation being the mother of inspiration, at a certain point I just started removing the MSBuild parameters, one by one, each time trying the build again. An hour later, I ended up with a configuration that works! See if you can spot the difference:
/p:publishUrl=$(build.artifactstagingdirectory)\WebsiteName /p:DeployOnBuild=true /p:DeployDefaultTarget=WebPublish /p:WebPublishMethod=FileSystem /p:DeleteExistingFiles=true /p:MvcBuildViews=true /p:MarkWebConfigAssistFilesAsExclude=false /p:TransformWebConfigEnabled=False
Yup. All I did was remove the OutDir argument, and magically the build started working!
Turns out the OutDir parameter is, well, old. And I am not the only one to run into issues with OutDir; Peter Seale noticed one seven years ago. As far as I can tell, MSBuild has changed enough in the past seven years that the OutDir parameter is no longer required, and is indeed a hindrance. Or, possibly, something else changed; I don't have enough background knowledge to know for certain.
But, really, the OutDir parameter isn't the true problem, is it?
Return of the Cargo Cult
You might have missed this tidbit from earlier: in order to make this build definition, I copied an existing definition. More specifically, I copied an existing definition without fully understanding what it did, why it did it, or really anything about it. I assumed it would just work, and this assumption ended up kicking my butt.
Yep, you guessed it: in that moment, I fell victim to that old standby of software development anti-patterns: cargo cult programming. I saw the man directing the airplanes with sticks, and figured the sticks themselves must be summoning the airplanes (the sticks, in this case, being MSBuild parameters).
Obviously, cargo cult programming is bad. Obviously, it leads to problems down the road, in the form of "we don't know why this works but we can't remove it because then it stops working". Obviously, I should have avoided cargo cult thinking at all costs.
Shouldn't I have?
The problem is that the real world is more complex than any pithily-named "anti-pattern". We developers are under deadlines, obligations, commitments, and various other requirements that we are sometimes forced to cut corners. We have a directive to deliver working software, not perfect software. So of course I was going to take a perfectly-good build definition from another project and reuse it; what else could I reasonably do?
This particular time, it bit me. I lost two days because of this decision. Most of the time, though, cannibalizing off other working projects does the job just fine.
All Things are Modifiable
The problem with listening to dogmatic ideas ("cargo cult programming is bad and you should feel bad for doing it") is that it breeds fanatics, people who slavishly adhere to ideals that might not even be achievable in the first place. These people are the anti-pragmatic programmers, the ones who favor architecture over usability, cleverness over readability, and perfection over shipping a working product.
I wrote an entire damn series on anti-patterns, and you'd be wise to take my words with a grain of salt here. But the point I'm making is this: in software development, there are rarely rules that cannot be broken. Everything is negotiable, everything can be modified, and the only constant is that nothing is. Nothing is ever unchangeable. In this case, I broke the rule of "cargo cult programming should be avoided" and it turned out terribly for me.
I'd do it again, though. Sometimes we as developers have to make concessions to the real world. Concessions like copying a working build definition from another project without fully understanding what it did because it means I can get a CI/CD pipeline in place more quickly. It bit me, I lost two days, and all around it was a very frustrating experience. But I would absolutely do it again, in these circumstances, because most of the time it works just fine.
Don't be afraid of the things people say are "wrong". Don't be afraid of "rules" because they are common knowledge in our field. Do your own research, reach your own conclusions, solve your own problems. In our world, nothing is wrong or right; rather, everything is just a tool. The trick is to know when to use it.
But you might still want to be afraid of OutDir.