C
C#•4mo ago
astral

How to Build Multiple Binaries from Dotnet New Console?

I have the following:
$ dotnet new console
$ dotnet run
Hello, world!
$ dotnet new console
$ dotnet run
Hello, world!
I move Program.cs to src/Program.cs and it still seems to work. I replace src/Program.cs with src/echo.cs with the following:
using System;

class Echo {
public static void Main(string[] args) {
foreach (string arg in args) {
Console.Write(arg);
// Put space if not last argument
if (arg != args[args.Length - 1]) {
Console.Write(" ");
}
Console.WriteLine();
}
}
};
using System;

class Echo {
public static void Main(string[] args) {
foreach (string arg in args) {
Console.Write(arg);
// Put space if not last argument
if (arg != args[args.Length - 1]) {
Console.Write(" ");
}
Console.WriteLine();
}
}
};
This is note-box.csproj:
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

</Project>
I see there's the executable note-box and it behaves like echo when I pass args. When I add a second main entry point, src/true.cs:
class True {
public static void Main() {
System.exit(0);
}
};
class True {
public static void Main() {
System.exit(0);
}
};
It errors. The goal is to have echo and true be two different programs. Another solution that I'm not sure if will work is if I just have one entry point: src/note-box.cs:
using System;

class NoteBox {
public static void Main(string[] args) {
switch (args[0]) {
case "echo":
// Call echo
break;
case "true":
// Call true
break;
}
}
};
using System;

class NoteBox {
public static void Main(string[] args) {
switch (args[0]) {
case "echo":
// Call echo
break;
case "true":
// Call true
break;
}
}
};
This would do something like ./bin/note-box echo or ./bin/note-box true. If this approach is better, I would want to make symlinks to note-box and have the name of the link be what it calls. In C it's something like:
char *cmd = strrchr(argv[0], '/');
cmd = cmd ? cmd + 1 : argv[0];
char *cmd = strrchr(argv[0], '/');
cmd = cmd ? cmd + 1 : argv[0];
I'm not sure how to resolve link name of executable in a C# way.
12 Replies
Jimmacle
Jimmacle•4mo ago
if you want 2 programs you need 2 projects or one program with an argument to select the behavior like you have
astral
astralOP•4mo ago
I want two exe csproj should be able to be configured to build that The POSIX C library gives strrchr to resolve real name of link. e.g. If I ln -sf note-box bin/echo Calling ./bin/echo will results in strrchr(argv[0], '/') returning the name of the link. The idea is to be able to call the symbolic link ./bin/echo instead of ./bin/note-box echo It looks like each exe needs a csproj, so im learning towards the one main entry point approach I just don't know how to resolve link names in C# IO
Jimmacle
Jimmacle•4mo ago
i'm missing how strrchr has anything to do with symlinks that's a string search function Environment.GetCommandLineArgs() gives you the equivalent of argv in C
astral
astralOP•4mo ago
ah i see i looked into ISO / IEC 9899:1999 a little more and found strrchr returns points to last known path starting with what was specified in second arg From ISO/IEC 9899:1999
7.21.5.5 The strrchr function
Synopsis
1 #include <string.h>
char *strrchr(const char *s, int c);
Description
2 The strrchr function locates the last occurrence of c (converted to a char) in the
string pointed to by s. The terminating null character is considered to be part of the
string.
Returns
3 The strrchr function returns a pointer to the character, or a null pointer if c does not
occur in the string.
7.21.5.5 The strrchr function
Synopsis
1 #include <string.h>
char *strrchr(const char *s, int c);
Description
2 The strrchr function locates the last occurrence of c (converted to a char) in the
string pointed to by s. The terminating null character is considered to be part of the
string.
Returns
3 The strrchr function returns a pointer to the character, or a null pointer if c does not
occur in the string.
let's say I have this:
note-box.csproj
bin/
- note-box*
- echo@ -> note-box
- true@ -> note-box
- false@ -> note-box
note-box.csproj
bin/
- note-box*
- echo@ -> note-box
- true@ -> note-box
- false@ -> note-box
And the goal is calling ./bin/<util> shall invoke ./bin/note-box <util> to call that util.
Jimmacle
Jimmacle•4mo ago
is there a particular reason you want it to work like this compared to calling the program with an argument?
astral
astralOP•4mo ago
Yes. The idea is to be able to call ./bin/echo instead of ./bin/note-box echo ^^
Jimmacle
Jimmacle•4mo ago
but why?
astral
astralOP•4mo ago
because that is the design scope i want to try and write something like this https://github.com/astralchan/astral-box in c# / dotnet. something to tinker with ^^ Maybe System.Environment has something to read link path. I'll check ^^
Jimmacle
Jimmacle•4mo ago
i mean it does, the thing i told you to use will get you the command line arguments including the path used to run the executable the same way the first element of argv would
astral
astralOP•4mo ago
Okay, I'll try it out ^^ hmm, GetCommandLineArgs() gives me argv. I found Environment.CommandLine, which gives /home/amber/dox/dev/note-box.git/main/bin/Debug/net8.0/note-box.dll true. I'll see if I can tinker around and get only the second part of it I found a solution 😃
string cmd = Environment.CommandLine.Split(' ')[1];
string cmd = Environment.CommandLine.Split(' ')[1];
That only seemed to work when passing it as an arg i tried making a symlink and errored its out of bounds I tried this:
string linkName = Path.GetFileName(Environment.GetCommandLineArgs()[0]);
string linkName = Path.GetFileName(Environment.GetCommandLineArgs()[0]);
which gives me note-box.dll at the moment I just manually made the link:
cd bin
ln -sf note-box true
cd ..
dotnet build
./bin/true
cd bin
ln -sf note-box true
cd ..
dotnet build
./bin/true
This seems to work, too:
<Target Name="PostBuild" AfterTargets="PostBuild">
<PropertyGroup>
<BuildDir>$(OutputPath)/note-box/</BuildDir>
</PropertyGroup>

<!-- Ensure the builddir/note-box directory exists -->
<Exec Command="mkdir -p $(BuildDir)" />

<!-- Create symbolic links -->
<Exec Command="ln -sf note-box $(BuildDir)/true" />
<Exec Command="ln -sf note-box $(BuildDir)/false" />
<Exec Command="ln -sf note-box $(BuildDir)/echo" />
</Target>
<Target Name="PostBuild" AfterTargets="PostBuild">
<PropertyGroup>
<BuildDir>$(OutputPath)/note-box/</BuildDir>
</PropertyGroup>

<!-- Ensure the builddir/note-box directory exists -->
<Exec Command="mkdir -p $(BuildDir)" />

<!-- Create symbolic links -->
<Exec Command="ln -sf note-box $(BuildDir)/true" />
<Exec Command="ln -sf note-box $(BuildDir)/false" />
<Exec Command="ln -sf note-box $(BuildDir)/echo" />
</Target>
ill brb imma tinker with system.environment and see if theres something to read link names
Jimmacle
Jimmacle•4mo ago
i'm not sure why you're doing this, i thought you wanted the path used to execute the file? if you want the program arguments just use the args array you get in Main
astral
astralOP•3mo ago
I have this generator:
using Microsoft.CodeAnalysis;

namespace SourceGenerator;

[Generator]
public class NoteBoxGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Code generation

// List of utilities
string[] utils = {
"true",
"false",
"echo",
};

// Construct switch block
string switchBlock = string.Empty;
foreach (var util in utils)
{
switchBlock += $@"
case ""{util}"": ret = {util.First().ToString().ToUpper() + util[1..]}.{util.First().ToString().ToUpper() + util[1..] + "Main"}; break;
";
}

string source = $@"
using System;

namespace NoteBox;

class NoteBox
{{
static void Main(string[] args)
{{
int ret = 0;

// switch block - e.g.
switch (args[0])
{{
{switchBlock}
}}

Console.WriteLine(""Hello, NoteBox!"");

Environment.Exit(ret);
}}
}}
";

context.AddSource("NoteBox.g.cs", source);
}

public void Initialize(GeneratorInitializationContext context)
{
// Initialization
}
}
using Microsoft.CodeAnalysis;

namespace SourceGenerator;

[Generator]
public class NoteBoxGenerator : ISourceGenerator
{
public void Execute(GeneratorExecutionContext context)
{
// Code generation

// List of utilities
string[] utils = {
"true",
"false",
"echo",
};

// Construct switch block
string switchBlock = string.Empty;
foreach (var util in utils)
{
switchBlock += $@"
case ""{util}"": ret = {util.First().ToString().ToUpper() + util[1..]}.{util.First().ToString().ToUpper() + util[1..] + "Main"}; break;
";
}

string source = $@"
using System;

namespace NoteBox;

class NoteBox
{{
static void Main(string[] args)
{{
int ret = 0;

// switch block - e.g.
switch (args[0])
{{
{switchBlock}
}}

Console.WriteLine(""Hello, NoteBox!"");

Environment.Exit(ret);
}}
}}
";

context.AddSource("NoteBox.g.cs", source);
}

public void Initialize(GeneratorInitializationContext context)
{
// Initialization
}
}
I can't seem to get it to build - smthn with the csproj:
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
</ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
</ItemGroup>

</Project>
/home/amber/dox/dev/note-box.git/main/NoteBoxGenerator/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs(4,12): error CS0579: Duplicate 'global::System.Runtime.Versioning.TargetFrameworkAttribute' attribute [/home/amber/dox/dev/note-box.git/main/note-box.csproj]
/home/amber/dox/dev/note-box.git/main/NoteBoxGenerator/obj/Debug/net8.0/.NETCoreApp,Version=v8.0.AssemblyAttributes.cs(4,12): error CS0579: Duplicate 'global::System.Runtime.Versioning.TargetFrameworkAttribute' attribute [/home/amber/dox/dev/note-box.git/main/note-box.csproj]
This is csproj of one level up:
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<!-- Add NoteBoxGenerator -->
<ItemGroup>
<ProjectReference Include=".\NoteBoxGenerator\NoteBoxGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
</ItemGroup>

</Project>
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
</PropertyGroup>

<!-- Add NoteBoxGenerator -->
<ItemGroup>
<ProjectReference Include=".\NoteBoxGenerator\NoteBoxGenerator.csproj"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4"/>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.11.0" />
</ItemGroup>

</Project>
Want results from more Discord servers?
Add your server