Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

PCH source file rebuilt every time when certain filenames are used #2249

Open
bdbaddog opened this issue Jan 2, 2018 · 56 comments
Open

PCH source file rebuilt every time when certain filenames are used #2249

bdbaddog opened this issue Jan 2, 2018 · 56 comments

Comments

@bdbaddog
Copy link
Contributor

bdbaddog commented Jan 2, 2018

This issue was originally created at: 2008-11-01 23:58:39.
This issue was reported by: jchristian.

jchristian said at 2008-11-01 23:58:39

Scons Version: Using "vs_revamp" branch build off of the 1.1.0 release. I suspect this problem happens on the official 1.1.0 release, but I haven't checked it.

Compiler: VS2008

Defect Summary: In attempting to use precompiled headers in building a Windows project, I ran into a fairly obscure bug. Apparently, if the filename specified as the source file for the PCH generation begins with the letter 'c' or 'C', the build regenerates the PCH file every time the build is run...even if no files have changed. If I change that filename to begin with another letter, it works. Note that originally my PCH source file was called "CorePCH.cpp". In trying things, I found it could be shortened to just "C.cpp" and produce the problem.

Expected Behavior: On the second build, it should just return with an "up to date" status.

Below is my directory structure and the files needed to reproduce the issue. Note that once you get this all setup, you can make it work successfully by changing the filename C.cpp in the SConscript to just PCH.cpp or XCorePCH.cpp.

Let me know if you have questions.

Directory structure:

project
    |
    +--SConstruct
    |
    +--include
    |    |
    |    +-- core
    |         |
    |         +-- CorePCH.hpp
    |             Hello.hpp
    |
    +-- lib
         |
         +-- core
              |
              +-- CorePCH.cpp
                  Hello.cpp
                  SConscript 

SConscript:

import glob

# get all the build variables we need
Import("env", "buildroot", "project", "mode", "debugcflags", "releasecflags")
staticlibenv = env.Clone()

builddir = buildroot + "/" + project  # holds the build directory for this
project
targetpath = builddir  # holds the path to the executable in the build directory

# append the user's additional compile flags
# assume debugcflags and releasecflags are defined
if mode == "debug":
    staticlibenv.Append(CPPFLAGS=debugcflags)
else:
    staticlibenv.Append(CPPFLAGS=releasecflags)


# get source file list
srclst = map(lambda x: builddir + "/static/" + x, glob.glob("*.cpp"))

# specify the build directory
staticlibenv.BuildDir(builddir + "/static", ".", duplicate=0)

staticlibenv2 = staticlibenv.Clone()
staticlibenv2["PCHSTOP"] = "core/CorePCH.hpp"
# pch.cpp is beside this SConscript
staticlibenv2["PCH"] = staticlibenv2.PCH(builddir + "/static/C.cpp")[0]
print("here 0")
print(staticlibenv2["PCH"])
print("here 1")
additionalcflags = ["-Yl__xge_pch_symbol"]
staticlibenv2.Append(CPPFLAGS=additionalcflags)
staticlib = staticlibenv2.StaticLibrary(targetpath + "/static/core", source=srclst)

SConstruct:

# get the mode flag from the command line
# default to 'release' if the user didn't specify
mode = ARGUMENTS.get("mode", "debug")  # holds current mode
platform = ARGUMENTS.get("platform", "win32")  # holds current platform

# check if the user has been naughty: only 'debug' or 'release' allowed
if not (mode in ["debug", "release"]):
    print("Error: expected 'debug' or 'release', found: " + mymode)
    Exit(1)

# check if the user has been naughty: only 'win32' or 'xbox' allowed
if not (platform in ["win32", "xbox"]):
    print("Error: expected 'win32' or 'release', found: " + platform)
    Exit(1)


# tell the user what we're doing
print("**** Compiling in " + mode + " mode...")

debugcflags = [  # extra compile flags for debug
    "-Od",
    "-MDd",
    "-Ob0",
    "-Z7",
    "-W4",
    "-EHsc",
    "-GR",
    "-D_DEBUG",
    "-DUSE_PCH",
]
releasecflags = [  # extra compile flags for release
    "-O2",
    "-MD",
    "-Ob2",
    "-EHsc",
    "-GR",
    "-DNDEBUG",
    "-DUSE_PCH",
]

env = Environment()

env.Append(CPPPATH=["#include", "#ext/boost-1.34.1"])

if mode == "debug":
    env.Append(LINKFLAGS="/DEBUG")

buildroot = "#build/" + mode  # holds the root of the build directory tree

# make sure the sconscripts can get to the variables
Export("env", "mode", "platform", "debugcflags", "releasecflags")

# put all .sconsign files in one place
env.SConsignFile()

# specify the sconscript for myprogram
project = "lib/core"
SConscript(project + "/SConscript", exports=["buildroot", "project"])

CorePCH.hpp:

#ifndef CORE_PCH_HPP
#define CORE_PCH_HPP
 
#ifdef USE_PCH
#include <string>
#include <iostream>
#include "core/Hello.hpp"
#endif
 
#endif // CORE_PCH_HPP

CorePCH.cpp or C.cpp:

#include "core/CorePCH.hpp"

Hello.hpp:

#ifndef HELLO_HPP
#define HELLO_HPP
 
#ifndef USE_PCH
#include <string>
#endif
 
class Hello
{
    public:
       Hello(const std::string& msg);
       ~Hello();
       void sayIt();
       
   private:
      std::string m_msg; 
};
#endif // HELLO_HPP

Hello.cpp:

#include "core/CorePCH.hpp"
#ifndef USE_PCH
#include "core/Hello.hpp"
#include <iostream>
#endif
 
Hello::Hello(const std::string& msg):m_msg(msg)
{
}
 
Hello::~Hello()
{
}
 
void Hello::sayIt()
{
    std::cout << m_msg << std::endl;
}

jchristian said at 2008-11-02 00:08:53

I made this a P2 because even though it is obscure, it is hella hard to track down what is going on. The workaround is to simply use another filename but this isn't apparent until many hours have been wasted. As well, I suspect the fix will be trivial and from my experience P3 and P4 defects tend to get put off practically forever.

jchristian said at 2008-11-04 00:39:53

Created an attachment (id=531)
Zip file that creates the dir structure described in the defect

jchristian said at 2008-11-04 00:49:32

Upon further investigation I have found that the filenames A.cpp thru H.cpp do not work. I have confirmed that I.cpp thru Q.cpp work. And also X.cpp. Other filenames were not tested because I got tired of doing it. To test a different filename, simply rename C.cpp in the lib/core directory and then update the SConscript file in that same directory to use that filename.

My suspicion is that the dependencies are not being generated or kept because of some defect in parsing or concatenating the filename.

jchristian said at 2008-11-04 00:55:43

Hopefully this can be fixed for 1.2

gregnoel said at 2008-12-04 17:41:09

1.2 is frozen; issues that didn't make it into 1.2 are moved to 1.3.

stevenknight said at 2008-12-06 11:22:53

This issue erroneously had a target milestone put on it right away, so it never got discussed and assigned to someone specific. Consequently, it's been languishing in a 1.[23] + issues@scons bucket that no one actually looks at. Change the target milestone to -unspecified- so we can discuss this at the next Bug Party and get it prioritized appropriately.

gregnoel said at 2008-12-24 06:41:53

Bug party triage. Steven suggests that it might be an interaction with the heuristics for determining the drive letter, but otherwise nobody has a clue. Assign to Steven for research as a penalty for suggesting a possibility.

Next time let us assign the milestone and priority. If it doesn't pass through our process, it'll get lost as this one did.

jchristian said at 2008-12-24 12:26:00

Sorry about that, I'll do that in the future. This was my first issue submitted and I wasn't sure what to put on some of the fields :O

stevenknight said at 2009-11-10 18:00:19

stevenknight => issues@scons

gregnoel said at 2010-01-20 02:20:51

Bug party triage. This area has been heavily revamped. Bill to research and see if the problem still happens.

John, can you try your test case with the newest checkpoint (published yesterday) and see if it's been fixed? Add a note to this issue and let us (well, Bill, to be precise) if you still see the problem. Tks.

gregnoel said at 2010-07-21 16:58:11

Bug party triage. Bump this issue to P1 so it will be reviewed at next bug party.

jchristian attached testpch.zip at 2008-11-04 00:39:53.

Zip file that creates the dir structure described in the defect

@mwichmann
Copy link
Collaborator

Tagged this with variantdir because that (in the old form BuildDir) appears in the SConscript.

@mwichmann mwichmann added pch Pre-compiled header support and removed P1 labels Sep 28, 2021
@mwichmann
Copy link
Collaborator

Something about this sounds familiar - wasn't there some ordering issue? #2877 has one of these.

@bdbaddog
Copy link
Contributor Author

bdbaddog commented Nov 4, 2023

The original filer is using glob.glob('*.cpp') which doesn't guarantee order staying the same between runs... That could have something to do with this..

@bdbaddog
Copy link
Contributor Author

bdbaddog commented Nov 4, 2023

The original filer is using glob.glob('*.cpp') which doesn't guarantee order staying the same between runs... That could have something to do with this..

I've imported the zip file and updated for python3 and BuildDir->VariantDir change.
Here: https://github.com/bdbaddog/scons-bugswork/tree/main/2249

This also has the problem where the VariantDir()'s source is a parent of the variantdir..

staticlibenv.VariantDir(builddir+"/static", ".", duplicate=0)

With my updates and one more, it's not finding c.pch in static.

@bdbaddog
Copy link
Contributor Author

bdbaddog commented Nov 4, 2023

Not exactly sure what's the issue.

Here's the generated command line which get's run (after my changes.. I moved variant_dir to the SConscript call so the variantdir wasn't a child of the src dir).
When it gets run, it doesn't generated any files? (@jcbrill - any idea? I'm not the PCH expert by far)..

cl /Fobuild\debug\lib\core\static\C.obj /c build\debug\lib\core\static\C.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore/CorePCH.hpp /Fpbuild\debug\lib\core\static\C.pch

All my updates are available here: https://github.com/bdbaddog/scons-bugswork/tree/main/2249

@jcbrill
Copy link
Contributor

jcbrill commented Nov 4, 2023

When it gets run, it doesn't generated any files? (@jcbrill - any idea? I'm not the PCH expert by far)..

I'm not either. When proceeding, keep in mind I'm just trying to help.

The sample files do illustrate a problem though.

Something about this sounds familiar - wasn't there some ordering issue? #2877 has one of these.

There does appear to be a lexical ordering issue.

All my updates are available here: https://github.com/bdbaddog/scons-bugswork/tree/main/2249

Using a recent version of scons with the build files, I had to change one line in the SConscript file due to a "PCHSTOP construction variable must be a string" error.

Original:

staticlibenv2['PCHSTOP'] = File('core/CorePCH.hpp')

Modified:

staticlibenv2['PCHSTOP'] = str(File('core/CorePCH.hpp'))

Working thesis: Any generated pch file name that is lexically ordered before "core" fails while any generated pch file name that is lexically ordered after "core" passes.

File C.cpp (C.pch) is lexically ordered before core and fails.
File Z.cpp (Z.pch) is lexically ordered after core and passes.

The results from two builds with and environment dump and command-line options --taskmastertrace - --tree=all are included in the text files corD-fail.txt (source file corD.cpp) and corF-pass.txt. (source file corF.cpp).

corD-fail.txt
corF-pass.txt

File corD-fail.txt contains the results when using corD.cpp for the file name (pch_nodes = staticlibenv2.PCH("corD.cpp"))
File corF-fail.txt contains the results when using corF.cpp for the file name (pch_nodes = staticlibenv2.PCH("corF.cpp"))

corD-fail.txt

The tree for corD-fail.txt is:

+-.
  +-build
  | +-build\debug
  |   +-build\debug\lib
  |     +-build\debug\lib\core
  |       +-build\debug\lib\core\static
  |         +-build\debug\lib\core\static\corD.cpp
  |         +-build\debug\lib\core\static\corD.obj
  |         | +-build\debug\lib\core\static\corD.cpp
  |         | +-include\core\CorePCH.hpp
  |         | +-include\core\Hello.hpp
  |         | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         +-build\debug\lib\core\static\corD.pch
  |         | +-build\debug\lib\core\static\corD.cpp
  |         | +-include\core\CorePCH.hpp
  |         | +-include\core\Hello.hpp
  |         | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         +-build\debug\lib\core\static\core
  |         | +-build\debug\lib\core\static\core\CorePCH.hpp
  |         +-build\debug\lib\core\static\core.lib
  |         | +-build\debug\lib\core\static\Hello.obj
  |         | | +-build\debug\lib\core\static\Hello.cpp
  |         | | +-build\debug\lib\core\static\corD.pch
  |         | | | +-build\debug\lib\core\static\corD.cpp
  |         | | | +-include\core\CorePCH.hpp
  |         | | | +-include\core\Hello.hpp
  |         | | | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         | | +-include\core\CorePCH.hpp
  |         | | +-include\core\Hello.hpp
  |         | | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         | +-build\debug\lib\core\static\corD.obj
  |         | | +-build\debug\lib\core\static\corD.cpp
  |         | | +-include\core\CorePCH.hpp
  |         | | +-include\core\Hello.hpp
  |         | | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\lib.EXE
  |         +-build\debug\lib\core\static\Hello.cpp
  |         +-build\debug\lib\core\static\Hello.obj
  |         | +-build\debug\lib\core\static\Hello.cpp
  |         | +-build\debug\lib\core\static\corD.pch
  |         | | +-build\debug\lib\core\static\corD.cpp
  |         | | +-include\core\CorePCH.hpp
  |         | | +-include\core\Hello.hpp
  |         | | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         | +-include\core\CorePCH.hpp
  |         | +-include\core\Hello.hpp
  |         | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         +-build\debug\lib\core\static\SConscript
  +-include
  | +-include\core
  |   +-include\core\CorePCH.hpp
  |   +-include\core\Hello.hpp
  +-lib
  | +-lib\core
  |   +-lib\core\corD.cpp
  |   +-lib\core\core
  |   | +-lib\core\core\CorePCH.hpp
  |   +-lib\core\Hello.cpp
  |   +-lib\core\SConscript
  +-SConstruct

corF-pass.txt

The tree for corF-pass.txt is:

+-.
  +-build
  | +-build\debug
  |   +-build\debug\lib
  |     +-build\debug\lib\core
  |       +-build\debug\lib\core\static
  |         +-build\debug\lib\core\static\core
  |         | +-build\debug\lib\core\static\core\CorePCH.hpp
  |         +-build\debug\lib\core\static\core.lib
  |         | +-build\debug\lib\core\static\Hello.obj
  |         | | +-build\debug\lib\core\static\Hello.cpp
  |         | | +-build\debug\lib\core\static\corF.pch
  |         | | | +-build\debug\lib\core\static\corF.cpp
  |         | | | +-include\core\CorePCH.hpp
  |         | | | +-include\core\Hello.hpp
  |         | | | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         | | +-include\core\CorePCH.hpp
  |         | | +-include\core\Hello.hpp
  |         | | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         | +-build\debug\lib\core\static\corF.obj
  |         | | +-build\debug\lib\core\static\corF.cpp
  |         | | +-include\core\CorePCH.hpp
  |         | | +-include\core\Hello.hpp
  |         | | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\lib.EXE
  |         +-build\debug\lib\core\static\corF.cpp
  |         +-build\debug\lib\core\static\corF.obj
  |         | +-build\debug\lib\core\static\corF.cpp
  |         | +-include\core\CorePCH.hpp
  |         | +-include\core\Hello.hpp
  |         | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         +-build\debug\lib\core\static\corF.pch
  |         | +-build\debug\lib\core\static\corF.cpp
  |         | +-include\core\CorePCH.hpp
  |         | +-include\core\Hello.hpp
  |         | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         +-build\debug\lib\core\static\Hello.cpp
  |         +-build\debug\lib\core\static\Hello.obj
  |         | +-build\debug\lib\core\static\Hello.cpp
  |         | +-build\debug\lib\core\static\corF.pch
  |         | | +-build\debug\lib\core\static\corF.cpp
  |         | | +-include\core\CorePCH.hpp
  |         | | +-include\core\Hello.hpp
  |         | | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         | +-include\core\CorePCH.hpp
  |         | +-include\core\Hello.hpp
  |         | +-C:\Software\MSVS-2022-143-Com\VC\Tools\MSVC\14.37.32822\bin\HostX64\x64\cl.EXE
  |         +-build\debug\lib\core\static\SConscript
  +-include
  | +-include\core
  |   +-include\core\CorePCH.hpp
  |   +-include\core\Hello.hpp
  +-lib
  | +-lib\core
  |   +-lib\core\core
  |   | +-lib\core\core\CorePCH.hpp
  |   +-lib\core\corF.cpp
  |   +-lib\core\Hello.cpp
  |   +-lib\core\SConscript
  +-SConstruct

Hope this points someone in the right direction.

Edit: Does the lexical ordering issue indicate a potential dependency issue?

@mwichmann
Copy link
Collaborator

The original filer is using glob.glob('*.cpp') which doesn't guarantee order staying the same between runs... That could have something to do with this..

I guess we've each "ported" this example in different ways...

Yes the sort order seems to matter, but it's not caused by using glob here (1) - the list ends up (for me at least) in alphabetical order anyway, and the key seems to be that when the source to be fed to the PCH builder is later in the source list it works, and when it's first it doesn't. I ran the example of taking a "working" setup and sorting the list in reverse, and it goes back to failing. That is:

# Working:

DEBUG: ['#build/debug/lib/core/static/Hello.cpp', '#build/debug/lib/core/static/T.cpp']
scons: done reading SConscript files.
scons: Building targets ...
cl /Fobuild\debug\lib\core\static\T.obj /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /c lib\core\T.cpp /Yccore/CorePCH.hpp /Fpbuild\debug\lib\core\static\T.pch T.cpp
cl /Fobuild\debug\lib\core\static\Hello.obj /c lib\core\Hello.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore/CorePCH.hpp /Fpbuild\debug\lib\core\static\T.pch Hello.cpp
lib /nologo /OUT:build\debug\lib\core\static\core.lib build\debug\lib\core\static\Hello.obj build\debug\lib\core\static\T.obj
scons: done building targets.

# Not working:

DEBUG: ['#build/debug/lib/core/static/T.cpp', '#build/debug/lib/core/static/Hello.cpp']
scons: done reading SConscript files.
scons: Building targets ...
cl /Fobuild\debug\lib\core\static\T.obj /c lib\core\T.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore/CorePCH.hpp /Fpbuild\debug\lib\core\static\T.pch T.cpp
lib\core\T.cpp(1): fatal error C1083: Cannot open precompiled header file: 'build\debug\lib\core\static\T.pch': No such file or directory

What's head-scratching there is the line which should end up building the pch comes out ordered a bit differently, though it appears to have identical contents.

The two dep tree dumps @jcbrill posted have the same contents, as well, accounting for the different filename. The sort order differs, I don't know if that's because we sort the deps when we emit the tree dump, or whether SCons actually has an internal difference that matters.

(1) footnote: I imagine this example uses glob.glob instead of Glob because the author is convinced of needing to do all this path fiddling, Trying to not let that get in the way of thinking about this thing...

@jcbrill
Copy link
Contributor

jcbrill commented Nov 4, 2023

Bear in mind that I don't know what I'm talking about.

The runs that pass seem to consider node build\\debug\\lib\\core\\static\\core before any individual file nodes (e.g., build\\debug\\lib\\core\\static\\corF.cpp).

What does the node build\\debug\\lib\\core\\static\\core represent? I'm guessing it is the static library itself.

In runs that fail, the taskmaster evaluates child file nodes before the "root" node.

Passes:

Taskmaster:      adjusted ref count: <pending    1   'build\\debug\\lib\\core'>, child 'build\\debug\\lib\\core\\static'
Taskmaster:     Considering node <no_state   0   'build\\debug\\lib\\core\\static'> and its children:
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\core'>         # <- root object?
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\core.lib'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\corF.cpp'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\corF.obj'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\corF.pch'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\Hello.cpp'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\Hello.obj'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\SConscript'>
Taskmaster:      adjusted ref count: <pending    1   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\core'
Taskmaster:      adjusted ref count: <pending    2   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\core.lib'
Taskmaster:      adjusted ref count: <pending    3   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\corF.cpp'
Taskmaster:      adjusted ref count: <pending    4   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\corF.obj'
Taskmaster:      adjusted ref count: <pending    5   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\corF.pch'
Taskmaster:      adjusted ref count: <pending    6   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\Hello.cpp'
Taskmaster:      adjusted ref count: <pending    7   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\Hello.obj'
Taskmaster:      adjusted ref count: <pending    8   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\SConscript'
Taskmaster:     Considering node <no_state   0   'build\\debug\\lib\\core\\static\\core'> and its children:
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\core\\CorePCH.hpp'>
Taskmaster:      adjusted ref count: <pending    1   'build\\debug\\lib\\core\\static\\core'>, child 'build\\debug\\lib\\core\\static\\core\\CorePCH.hpp'
Taskmaster:     Considering node <no_state   0   'build\\debug\\lib\\core\\static\\core\\CorePCH.hpp'> and its children:
Taskmaster: Evaluating <pending    0   'build\\debug\\lib\\core\\static\\core\\CorePCH.hpp'>

Fails:

Taskmaster:     Considering node <no_state   0   'build\\debug\\lib\\core\\static'> and its children:
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\corD.cpp'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\corD.obj'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\corD.pch'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\core'>            # <-- root object?
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\core.lib'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\Hello.cpp'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\Hello.obj'>
Taskmaster:        <no_state   0   'build\\debug\\lib\\core\\static\\SConscript'>
Taskmaster:      adjusted ref count: <pending    1   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\corD.cpp'
Taskmaster:      adjusted ref count: <pending    2   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\corD.obj'
Taskmaster:      adjusted ref count: <pending    3   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\corD.pch'
Taskmaster:      adjusted ref count: <pending    4   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\core'
Taskmaster:      adjusted ref count: <pending    5   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\core.lib'
Taskmaster:      adjusted ref count: <pending    6   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\Hello.cpp'
Taskmaster:      adjusted ref count: <pending    7   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\Hello.obj'
Taskmaster:      adjusted ref count: <pending    8   'build\\debug\\lib\\core\\static'>, child 'build\\debug\\lib\\core\\static\\SConscript'
Taskmaster:     Considering node <no_state   0   'build\\debug\\lib\\core\\static\\corD.cpp'> and its children:
Taskmaster: Evaluating <pending    0   'build\\debug\\lib\\core\\static\\corD.cpp'>

@jcbrill
Copy link
Contributor

jcbrill commented Nov 4, 2023

Just a WAG and likely misinformation, but it seems the difference in evaluation order of the DAG could be responsible for the difference in behavior. Maybe?

@mwichmann
Copy link
Collaborator

Bear in mind that I don't know what I'm talking about.

The runs that pass seem to consider node build\\debug\\lib\\core\\static\\core before any individual file nodes (e.g., build\\debug\\lib\\core\\static\\corF.cpp).

In a case-insenstive sort, which I assume SCons thinks it needs to do because it's a Windows file system, "core" is before corF". It's probably not more than that... although this seems the wrong place to be worrying about sort order...

What does the node build\\debug\\lib\\core\\static\\core represent? I'm guessing it is the static library itself.

This is probably because SCons lets you call targets by un-suffixed names, aka StaticLibrary(target="core", source=some-list), and so it wants to keep track of that as well as the produced output file name. At this point /me/ just guessing, though.

@mwichmann
Copy link
Collaborator

We always say "the taskmaster will figure it out", but there have been more than one case where things that are essentially "side effects" which also become sources don't work out right - I think there are several closed PCH bugs about that problem, maybe it wasn't completely stamped out in the past...

@jcbrill
Copy link
Contributor

jcbrill commented Nov 4, 2023

What's head-scratching there is the line which should end up building the pch comes out ordered a bit differently, though it appears to have identical contents.

The lines to use (/Yu) the generated pch file are identical in both listings.

The line to create (/Yc) the generated pch only appears in the first listing:

cl /Fobuild\debug\lib\core\static\T.obj /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /c lib\core\T.cpp /Yccore/CorePCH.hpp /Fpbuild\debug\lib\core\static\T.pch T.cpp

@mwichmann
Copy link
Collaborator

You're absolutely right, that is the difference, it fooled my old tired eyes, because after I moved the /c ... stanza in one copy, they were the same length and looked the same, but they're not. Sigh. Thanks for spotting.

/Yccore/CorePCH.hpp  # <- working
/Yucore/CorePCH.hpp  # <- not working

@jcbrill
Copy link
Contributor

jcbrill commented Nov 4, 2023

Stupid question time.

In the abstract, it appears that there is a difference when processing the DAG by the taskmaster whether or not the static library node is considered first or not.

Is it possible that the the pch source file and target are "disconnected" from the DAG when evaluated prior to the static library node?

That is, is it possible that the pch source/target is evaluated before a parent/child relationship is established by processing the static library node first?

I don't know anything about how the DAG is constructed. "Construct and then evaluate" is different from "construct and evaluate during discovery". In the latter case, starting/root nodes might matter.

@mwichmann
Copy link
Collaborator

mwichmann commented Nov 4, 2023

This also has the problem where the VariantDir()'s source is a parent of the variantdir..

staticlibenv.VariantDir(builddir+"/static", ".", duplicate=0)

@bdbaddog you keep saying this, but there are so many testcases that do exactly this, as well as documentation and examples, that I can't help but think it was intended to work... just from a quick search:

SCons/Environment.xml:VariantDir('build', '.', duplicate=0)
SCons/Script/SConscript.xml:SConscript(dirs = 'src', variant_dir = 'build', src_dir = '.')
SCons/Script/SConscript.xml:VariantDir('build', '.') 

SCons/Node/FSTests.py:        fs.VariantDir('build', '.')
test/Configure/VariantDir2.py:SConscript('SConscript', variant_dir='build', src='.')
test/Configure/VariantDir.py:VariantDir('build', '.')
test/Configure/VariantDir-SConscript.py:VariantDir( 'build', '.' )
testing/framework/TestSCons.py:    VariantDir(builddir, '.', duplicate=dup)
test/option/option--duplicate.py:VariantDir('build', '.', duplicate=1)
test/QT/qt3/installed.py:VariantDir('bld', '.')
test/QT/qt3/QTFLAGS.py:    VariantDir('build', '.', duplicate=1)
test/VariantDir/Clean.py:VariantDir('build0', '.', duplicate=0)
test/VariantDir/Clean.py:VariantDir('build1', '.', duplicate=1)
test/VariantDir/guess-subdir.py:VariantDir(c_builddir, '.', duplicate=0)
test/VariantDir/SConscript-variant_dir.py:SConscript('SConscript', variant_dir='Build', src_dir='.', duplicate=0)
test/VariantDir/SConscript-variant_dir.py:SConscript('src/SConscript', variant_dir='build/var9', src_dir='.')
test/VariantDir/SConscript-variant_dir.py:VariantDir('build/var9', '.')
test/VariantDir/VariantDir.py:VariantDir('build', '.')
test/VariantDir/VariantDir.py:VariantDir('build', '.', duplicate=1 ) 

@mwichmann
Copy link
Collaborator

mwichmann commented Nov 4, 2023

I've wasted a little more time poking at this, and I'm pretty convinced everything is consistent coming out of the sconscript phase - the PCH builder has been invoked the same way and produced the same result - and that the problem indeed happens at taskmaster time. Won't put any money on it, though :-)

@jcbrill
Copy link
Contributor

jcbrill commented Nov 5, 2023

@mwichmann When you have time, would you please review to see if the following makes any sense.

My comment about the commands in your runs is not quite true. In both runs the first compiler command is for the pch file but with different switches for pch (/Yc vs /Yu).

The difference between the builds that fail and the builds that pass is that in the failed builds the first generated compile command is not creating the precompiled header files but instead is generated to use the precompiled header file.

I tried another pair of tests. The file names were changed so they both lexically precede "core".

Test 1 - [pch source file discovered first]:
a-pch.cpp [formerly C.cpp]
b-hello.cpp [formerly Hello.cpp]

Test 1 - Fails:

cl /Fobuild\debug\lib\core\static\a-pch.obj /c build\debug\lib\core\static\a-pch.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore\CorePCH.hpp /Fpbuild\debug\lib\core\static\a-pch.pch

The pre-compiled header source file command-line should be creating (/Yc) the pch header instead of using (/Yu) the pch header.

Test 2 - [pch source file discovered after source file]:
b-pch.cpp [formerly C.cpp]
a-hellp.cpp [formerly Hello.cpp]

Test 2 - Passes:

cl /Fobuild\debug\lib\core\static\b-pch.obj /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /c build\debug\lib\core\static\b-pch.cpp /Yccore\CorePCH.hpp /Fpbuild\debug\lib\core\static\b-pch.pch
cl /Fobuild\debug\lib\core\static\a-hello.obj /c build\debug\lib\core\static\a-hello.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore\CorePCH.hpp /Fpbuild\debug\lib\core\static\b-pch.pch

The precompiled header source file command-line is creating (/Yu) the pch header.

Observation:
In the fail build, the pch source file is the first compiled and the command-line is using (/Yu) the compiled header file.
In the pass build, the pch source file is the first compiled and the command-line is creating (/Yc) the compiled header file.

When the pch source file is discovered first, the command-line is not generated with the create (/Yc) option.

From your run above, the first compiler calls from both runs are shown (manually adjusted the options order so they line-up visually):

cl /Fobuild\debug\lib\core\static\T.obj /c lib\core\T.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yccore/CorePCH.hpp /Fpbuild\debug\lib\core\static\T.pch T.cpp
cl /Fobuild\debug\lib\core\static\T.obj /c lib\core\T.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore/CorePCH.hpp /Fpbuild\debug\lib\core\static\T.pch T.cpp

The only difference is the successful build creates (/Yc) the pch file while the failed build uses (/Yc) the pch file.

@jcbrill
Copy link
Contributor

jcbrill commented Nov 5, 2023

Based on the generated argument order and pch switch difference for the pch source file, I'm starting to think that PCHCOM is not called/applied when the pch source file is discovered first:

cl /Fobuild\debug\lib\core\static\b-pch.obj /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /c build\debug\lib\core\static\b-pch.cpp /Yccore\CorePCH.hpp /Fpbuild\debug\lib\core\static\b-pch.pch

cl /Fobuild\debug\lib\core\static\a-pch.obj /c build\debug\lib\core\static\a-pch.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore\CorePCH.hpp /Fpbuild\debug\lib\core\static\a-pch.pch

@bdbaddog
Copy link
Contributor Author

bdbaddog commented Nov 5, 2023

The problem with having the variant dir be a child of the source is that then on the second run the build dir itself is the source for build/build.. and so on iteratively... It's just a BAD idea.
This can lead to really difficult to debug situations.. so I always advise not to do it.
There's little to gain beside some insistance on a structure which is problematic.

Now take the above and add more than one variant where all the N variants are children of the source dir..

If it happens to work sometimes, that doesn't in any way mean it's a good idea.

@mwichmann
Copy link
Collaborator

@mwichmann When you have time, would you please review to see if the following makes any sense.

The difference between the builds that fail and the builds that pass is that in the failed builds the first generated compile command is not creating the precompiled header files but instead is generated to use the precompiled header file.

Yes, this is what I was saying, and also that there doesn't seem to be any difference coming out of the emitter in the PCH builder, so there's some other problem in the plumbing. Not sure how well the PCH logic understands the current model anyway. We have a construction variable (which this example sets):

https://scons.org/doc/production/HTML/scons-man.html#cv-PCH

which is per-environment, but that's not really a requirement:

Although you can use only one precompiled header (.pch) file per source file, you can use multiple .pch files in a project.

@jcbrill
Copy link
Contributor

jcbrill commented Nov 5, 2023

Old-school print-style debugging.

The builds that pass generate a command to build the pch and obj file.
The builds that fail generate a command to build the obj file.

PASS

Building build\debug\lib\core\static\b-pch.pch and build\debug\lib\core\static\b-pch.obj with action:
  $CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS

ACTION.PROCESS 
  $CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS 
  [['cl', '/Fobuild\\debug\\lib\\core\\static\\b-pch.obj', '/TP', '/nologo', '-Od', '-MDd', '-Ob0', '-Z7', '-W4', '-EHsc', '-GR', '-D_DEBUG', '-DUSE_PCH', '-Yl__xge_pch_symbol', '/Iinclude', '/c', 'build\\debug\\lib\\core\\static\\b-pch.cpp', '/Yccore\\CorePCH.hpp', '/Fpbuild\\debug\\lib\\core\\static\\b-pch.pch']] 
  None 
  None

cl /Fobuild\debug\lib\core\static\b-pch.obj /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /c build\debug\lib\core\static\b-pch.cpp /Yccore\CorePCH.hpp /Fpbuild\debug\lib\core\static\b-pch.pch

FAIL

Building build\debug\lib\core\static\a-pch.obj with action:
  ${TEMPFILE("$CXX $_MSVC_OUTPUT_FLAG /c $CHANGED_SOURCES $CXXFLAGS $CCFLAGS $_CCCOMCOM","$CXXCOMSTR")}

...

ACTION.PROCESS 
  ${TEMPFILE("$CXX $_MSVC_OUTPUT_FLAG /c $CHANGED_SOURCES $CXXFLAGS $CCFLAGS $_CCCOMCOM","$CXXCOMSTR")} 
  [['cl', '/Fobuild\\debug\\lib\\core\\static\\a-pch.obj', '/c', 'build\\debug\\lib\\core\\static\\a-pch.cpp', '/TP', '/nologo', '-Od', '-MDd', '-Ob0', '-Z7', '-W4', '-EHsc', '-GR', '-D_DEBUG', '-DUSE_PCH', '-Yl__xge_pch_symbol', '/Iinclude', '/Yucore\\CorePCH.hpp', '/Fpbuild\\debug\\lib\\core\\static\\a-pch.pch']] 
  None 
  None

cl /Fobuild\debug\lib\core\static\a-pch.obj /c build\debug\lib\core\static\a-pch.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore\CorePCH.hpp /Fpbuild\debug\lib\core\static\a-pch.pch

@mwichmann
Copy link
Collaborator

No, I don't get this bit either. Usually, you find header deps by scanning the source file, but that doesn't happen in the case of Microsoft's PCH, as those only come from the command line (/Yu', and /Fp` in case you don't want the default pch filename). And since we're the ones supplying the command line, how do we tell?

@jcbrill
Copy link
Contributor

jcbrill commented Nov 5, 2023

And since we're the ones supplying the command line, how do we tell?

I haven't the foggiest idea (yet).

I tried a handful of ways to manually add a dependency via "Depends". Everything yielded the header missing error or a cycle detected error.

I may not have done it correctly but it makes me wonder if manually added a dependency causes the deferred dependency to be discovered. I'm not confident.

You might have better luck trying for the runs that fail.

@jcbrill
Copy link
Contributor

jcbrill commented Nov 5, 2023

If the PCH variables are defined following the StaticLibrary call (i.e., after the source files have been registered) the build succeeds.

I'm not saying this is a good idea and probably fails for some other reason in a real-world scenario, but it definitely illustrates that the DAG is processed/constructed differently between the two cases.

When DEFER_PCH=True the build succeeds.
When DEFER_PCH=False the build fails.

SConscript file:

import glob

DEFER_PCH = True

#get all the build variables we need
Import('env', 'buildroot', 'project', 'mode', 'debugcflags', 'releasecflags')
staticlibenv = env.Clone()

builddir = buildroot + '/' + project   #holds the build directory for this project
targetpath = builddir  #holds the path to the executable in the build directory

#append the user's additional compile flags
#assume debugcflags and releasecflags are defined
if mode == 'debug':
   staticlibenv.Append(CPPFLAGS=debugcflags)
else:
   staticlibenv.Append(CPPFLAGS=releasecflags)

#get source file list
srclst = Glob('*.cpp')

staticlibenv2 = staticlibenv.Clone()

if not DEFER_PCH:
    staticlibenv2['PCHSTOP'] = str(File('core/CorePCH.hpp'))
    # pch.cpp is beside this SConscript
    pch_nodes = staticlibenv2.PCH("a-pch.cpp")
    print("PCHN: %s"%pch_nodes)
    staticlibenv2['PCH'] = pch_nodes[0]

additionalcflags = ['-Yl__xge_pch_symbol']
staticlibenv2.Append(CPPFLAGS=additionalcflags)

staticlib = staticlibenv2.StaticLibrary("core", source=srclst)

if DEFER_PCH:
    staticlibenv2['PCHSTOP'] = str(File('core/CorePCH.hpp'))
    # pch.cpp is beside this SConscript
    pch_nodes = staticlibenv2.PCH("a-pch.cpp")
    print("PCHN: %s"%pch_nodes)
    staticlibenv2['PCH'] = pch_nodes[0]

fail.txt
pass.txt

@mwichmann
Copy link
Collaborator

Interesting info. Particularly odd since in the case where the order puts the non-pch-source file first alphabetically, the call to the PCH builder still takes place before the StaticLibrary builder call. I don't know how worthwhile this is to pursue further - "it's broken" on some level might be as far as it goes. I know MS pushed PCH very hard for a while, but it hasn't come up a huge amount in SCons usage, as there seem to just a few reports stretching over a very long time period.

@jcbrill
Copy link
Contributor

jcbrill commented Nov 5, 2023

Agreed. This was far more interesting yesterday and becomes less so with each hour spent.

@jcbrill
Copy link
Contributor

jcbrill commented Nov 6, 2023

@mwichmann I believe (most of) the behavior can be explained.

The issue arises when both the pch source file is added to the source list and the env.PCH() method is called.

The short answer is: don't add the pch source file to the source file list when calling env.PCH().

Observations:

  • There are no issues when the pch source file is removed from the source list (Build 1 and 2 in the table below).
  • There are ordering issues when the pch source file is not removed from source list (Build 3 and 4 in the table below).
Build PCH.cpp Hellp.CPP Remove PCH.CPP Result First Node Output
1 a-pch.cpp b-hello.cpp True Pass a-pch.pch a-pch.pch, a-pch.obj
2 b-pch.cpp a-hello.cpp True Pass b-pch.pch b-pch.pch, b-pch.obj
3 a-pch.cpp b-hello.cpp False Fail a-pch.obj a-pch.obj
4 b-pch.cpp a-hello.cpp False Pass b-pch.pch b-pch.pch, b-pch.obj

There does not appear to be an inherent flaw in the taskmaster. There is however an ordering problem, and possibly a missing dependency, when the pch source file appears in the source list.

For the build that fails, the node ready sequence is evaluated in a different order considering a-pch.obj before a-pch.pch.

At present, I'm not sure how to what it would take to alter this behavior.

SConscript:

import glob

PCH_FILE_NAME = 'a-pch.cpp'
PCH_FILE = File(PCH_FILE_NAME) # remove from Glob srclist
PCH_FILE_REMOVE = True

#get all the build variables we need
Import('env', 'buildroot', 'project', 'mode', 'debugcflags', 'releasecflags')
staticlibenv = env.Clone()

builddir = buildroot + '/' + project   #holds the build directory for this project
targetpath = builddir  #holds the path to the executable in the build directory

#append the user's additional compile flags
#assume debugcflags and releasecflags are defined
if mode == 'debug':
   staticlibenv.Append(CPPFLAGS=debugcflags)
else:
   staticlibenv.Append(CPPFLAGS=releasecflags)

#get source file list
srclst = Glob('*.cpp')

if PCH_FILE_REMOVE:
    srclst.remove(PCH_FILE)

staticlibenv2 = staticlibenv.Clone()

staticlibenv2['PCHSTOP'] = 'core/CorePCH.hpp'
# pch.cpp is beside this SConscript
pch_nodes = staticlibenv2.PCH(PCH_FILE_NAME)
staticlibenv2['PCH'] = pch_nodes[0]

additionalcflags = ['-Yl__xge_pch_symbol']
staticlibenv2.Append(CPPFLAGS=additionalcflags)

staticlib = staticlibenv2.StaticLibrary("core", source=srclst)

Build 3 - a-pch.obj node sequence (plenty of text removed between lines shown):

BUILDER 5 INIT $PCHCOMSTR

BUILDER 17 INIT 

NODE (19, 'a-pch.obj') FS.FILE INIT

EXECUTOR 11 INIT '$CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS' [([(18, 'a-pch.pch'), (19, 'a-pch.obj')], [(14, 'a-pch.cpp')])]

NODE 19 BUILDER_SET (5, '$CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS')

MSVC OBJECT_EMITTER 
  [(19, 'a-pch.obj')] [(14, 'a-pch.cpp')]

COMMON GET_PCH_NODE 
  [(19, 'a-pch.obj')] [(14, 'a-pch.cpp')] 
  False (0, 'a-pch.pch')

EXECUTOR 12 INIT '' [([(19, 'a-pch.obj')], [(14, 'a-pch.cpp')])]

NODE 19 BUILDER_SET (17, '')

NODE 19 DEL EXECUTOR 11

NODE 19 GET_EXECUTOR 12

EXECUTOR 12 GET_ALL_TARGETS [(19, 'build\\debug\\lib\\core\\static\\a-pch.obj')]

ACTION.PROCESS 
  ${TEMPFILE("$CXX $_MSVC_OUTPUT_FLAG /c $CHANGED_SOURCES $CXXFLAGS $CCFLAGS $_CCCOMCOM","$CXXCOMSTR")} 
  [['cl', '/Fobuild\\debug\\lib\\core\\static\\a-pch.obj', '/c', 'build\\debug\\lib\\core\\static\\a-pch.cpp', '/TP', '/nologo', '-Od', '-MDd', '-Ob0', '-Z7', '-W4', '-EHsc', '-GR', '-D_DEBUG', '-DUSE_PCH', '-Yl__xge_pch_symbol', '/Iinclude', '/Yucore/CorePCH.hpp', '/Fpbuild\\debug\\lib\\core\\static\\a-pch.pch']] 

cl /Fobuild\debug\lib\core\static\a-pch.obj /c build\debug\lib\core\static\a-pch.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore/CorePCH.hpp /Fpbuild\debug\lib\core\static\a-pch.pch

build\debug\lib\core\static\a-pch.cpp(1): fatal error C1083: Cannot open precompiled header file: 'build\debug\lib\core\static\a-pch.pch': No such file or directory

Build 4 - b-pch.pch node sequence (plenty of text removed between lines shown):

BUILDER 5 INIT $PCHCOMSTR

NODE (18, 'b-pch.pch') FS.ENTRY INIT

NODE 18 BUILDER_SET (5, '$CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS')

MSVC PCH_EMITTER 
  b-pch.pch b-pch.obj 
  [(18, 'b-pch.pch'), (0, 'b-pch.obj')] [(14, 'b-pch.cpp')]
  
NODE 18 BUILDER_SET (0, '')

EXECUTOR 11 INIT '$CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS' [([(18, 'b-pch.pch'), (19, 'b-pch.obj')], [(14, 'b-pch.cpp')])]

NODE 18 BUILDER_SET (5, '$CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS')

NODE 18 GET_EXECUTOR 11

EXECUTOR 11 GET_ALL_TARGETS [(18, 'build\\debug\\lib\\core\\static\\b-pch.pch'), (19, 'build\\debug\\lib\\core\\static\\b-pch.obj')]

ACTION.PROCESS 
  $CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS 
  [['cl', '/Fobuild\\debug\\lib\\core\\static\\b-pch.obj', '/TP', '/nologo', '-Od', '-MDd', '-Ob0', '-Z7', '-W4', '-EHsc', '-GR', '-D_DEBUG', '-DUSE_PCH', '-Yl__xge_pch_symbol', '/Iinclude', '/c', 'build\\debug\\lib\\core\\static\\b-pch.cpp', '/Yccore/CorePCH.hpp', '/Fpbuild\\debug\\lib\\core\\static\\b-pch.pch']] 

cl /Fobuild\debug\lib\core\static\b-pch.obj /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /c build\debug\lib\core\static\b-pch.cpp /Yccore/CorePCH.hpp /Fpbuild\debug\lib\core\static\b-pch.pch

Log files:

@mwichmann
Copy link
Collaborator

mwichmann commented Nov 6, 2023

Yum. Turns out there's a specific test for this case, as a result of previous failures, #2505, which I quoted from above (that's the "a little risky" line). The test is test/MSVC/PCH-source.py, and it opens with the comment:

Test use of pre-compiled headers when the source .cpp file shows               
up in both the env.PCH() and the env.Program() source list.                    

In the test code, I can take the key line and reorder the source list, like this:

#env.Program('foo', ['foo.cpp', 'Source2.cpp', 'Source1.cpp'])                 
env.Program('foo', ['Source1.cpp', 'Source2.cpp', 'foo.cpp'])  

And now the test fails!

scons: Building targets ...
cl /FoSource1.obj /c Source1.cpp /TP /nologo /YuHeader1.hpp /FpSource1.pch
Source1.cpp
Source1.cpp(4): fatal error C1083: Cannot open precompiled header file: 'Source1.pch': No such file or directory                                 
scons: building terminated because of errors.  

@jcbrill
Copy link
Contributor

jcbrill commented Nov 6, 2023

Small victories!

After some reflection, I think there is a (simple) explanation for the failed build..

Let me know if this sounds reasonable or if I should put on a tin-foil hat.

In the build listing above, the following modified fragment is the crux of the problem:

BUILDER 5 INIT $PCHCOMSTR

NODE 19 'a-pch.obj'

EXECUTOR 11 INIT '$CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS' [([(18, 'a-pch.pch'), (19, 'a-pch.obj')], [(14, 'a-pch.cpp')])]

NODE 19 BUILDER_SET (5, '$CXX /Fo${TARGETS[1]} $CXXFLAGS $CCFLAGS $CPPFLAGS $_CPPDEFFLAGS $_CPPINCFLAGS /c $SOURCES /Yc$PCHSTOP /Fp${TARGETS[0]} $CCPDBFLAGS $PCHPDBFLAGS')

# STATUS: `a-pch.obj` target=['a-pch.pch', 'a-pch.obj'], source=['a-pch.cpp']

EXECUTOR 12 INIT '' [([(19, 'a-pch.obj')], [(14, 'a-pch.cpp')])]

NODE 19 BUILDER_SET (17, '') # <-- new builder/executor assigned

# NODE 19 SET BUILDER 17 [EXECUTOR 12 when retrieved]

NODE 19 DEL EXECUTOR 11  # <-- PCHCOM executor cleared (executor is assigned on access)

# STATUS: `a-pch.obj`  target=['a-pch.obj'], source=['a-pch.cpp']

...

# NODE 19 first selected for compilation

NODE 19 GET_EXECUTOR 12

EXECUTOR 12 GET_ALL_TARGETS [(19, 'build\\debug\\lib\\core\\static\\a-pch.obj')]

#  OOPS `a-pch.obj` considered before `a-pch.pch`

The PCHCOM builder/executor is replaced with a generic builder/executor based on the node processing order. A node will only have a single executor.

When the a-pch.obj executor is replaced, the target list changed from [a-pch.pch, a-pch.obj] to [a-pch.obj].

Effectively, the output target dependency on a-pch.pch is removed.

I believe this means that there is no longer a dependency between the object file a-pch.obj and the generated a-pch.pch file.

Without this dependency, a-pch.obj can be considered before, at the same time, or after a-pch.pch.

Plausible?

@jcbrill
Copy link
Contributor

jcbrill commented Nov 6, 2023

Put a simpler way:

  • When the pch source file is not in the source list, 1 builder is assigned to the PCHFILE.obj node.
  • When the pch source file is in the source list, 2 builders are assigned to the PCHFILE.obj node. The second assignment removes the generated PCHFILE.pch file from the PCHFILE.obj node targets list. This is asking for trouble as now success or failure is dependent on the order in which candidate nodes are encountered during the DAG walk due to the missing relationship between the PCHFILE.obj and PCHFILE.pch.

Or at least that is what I've convinced myself is happening. Too much time spent down the rabbit hole...

@mwichmann
Copy link
Collaborator

Microsoft's PCH implementation seems quite awkward - even they admit this, sort of, for example (in a comparison of techniques): "PCH files have restrictions that make them difficult to maintain.". I can't wrap my head around how this works normally. The examples in the SCons world seem to be of the form foo.cc includes foo.h includes the headers to be precompiled. In these examples, foo.cc includes nothing else, so it makes no real sense to include that in the program being built anyway. I'm assuming reality is not always like that. Meanwhile, the way SCons includes the pch file is the msvc way: /Yu to use it and /Fp to specify the filename. Then it acts like a #include had been placed in the files being compiled this way - similar to how gcc's --include works - and interestingly, SCons can't handle that, since it depends on scanners to work out the dependency chain, and the scanner doesn't know about an include provided by the compiler that doesn't appear in the source code. Coincidence? Or related topic?

@jcbrill
Copy link
Contributor

jcbrill commented Nov 7, 2023

Fun fact: a second call to env.PCH() after env.Program() or env.StaticLibrary works in all test cases.

In the test code, I can take the key line and reorder the source list, like this:

#env.Program('foo', ['foo.cpp', 'Source2.cpp', 'Source1.cpp'])                 
env.Program('foo', ['Source1.cpp', 'Source2.cpp', 'foo.cpp'])  

If you add a second call to env.PCH() after env.Program() like this:

DefaultEnvironment(tools=[])
env = Environment(tools=['msvc', 'mslink'])
env['PCH'] = env.PCH('Source1.cpp')[0]
env['PCHSTOP'] = 'Header1.hpp'
#env.Program('foo', ['foo.cpp', 'Source2.cpp', 'Source1.cpp'])
env.Program('foo', ['Source1.cpp', 'Source2.cpp', 'foo.cpp'])
env.PCH('Source1.cpp')  # <-- SECOND PCH CALL

The test passes! It works for either configuration of the source lists.

Adding a second env.PCH() call after env.StaticLibrary() also works for all 4 test cases above.

Basically, the second call after the source list is registered in StaticLibrary re-establishes the targets for the PCHFILE.obj node.

This is recommended for the runs where the pch source file is in the source list. It doesn't appear to hurt in the runs where the pch source file is removed from the source list.

It does have to be done twice. A single invocation after the StaticLibrary call suffers the same ordering problem depending on the file names (i.e., one run will pass one run will fail).

@jcbrill
Copy link
Contributor

jcbrill commented Nov 7, 2023

Precompiled header standard disclaimers:

  • I am not a C++ programmer (although pch can be useful in c programs)
  • I have never used precompiled headers in my own work

It is more likely than not that the following is not technically correct. Please correct any and all misunderstandings and flat-out untruths.

Microsoft's PCH implementation seems quite awkward - even they admit this, sort of, for example (in a comparison of techniques): "PCH files have restrictions that make them difficult to maintain.".

I can see where precompiled headers would be valuable when using "heavyweight" third-party c++ libraries especially those with a ton of template metaprogramming and/or template specialization. For a local project, typically the third-party libraries would not be updated frequently. For example, c++ libraries like QT, wxWidgets, OGRE, Boost, etc., ....

Basically put all of the third-party includes and c/c++ system headers in a common include file and use a precompiled header file. It strikes me as more valuable for header files that are not developed in-house (e.g., windows headers, compiler system headers, third-party libraries).

I can't wrap my head around how this works normally. The examples in the SCons world seem to be of the form foo.cc includes foo.h includes the headers to be precompiled. In these examples, foo.cc includes nothing else, so it makes no real sense to include that in the program being built anyway. I'm assuming reality is not always like that.

In most of the examples, the pch header file is processed in its entirety so there is no reason to include the pch object file in the linker command. The pch header file does not necessarily have to be processed in it's entirety (e.g., the header stop is in the middle of the file). Given that the pch header is not processed in its entirety, maybe there is a reason for the including the pch source file and linking with the pch object file. Seems sketchy.

There seems to be a variety of methods of using precompiled header files:

  • The first line of all source files (e.g., non-header files) includes the pch source header file.
  • The source pch header file is not present in the source files and is "force included" from the command-line (/FI)
  • The source files include header files in the normal fashion with those that are in the source pch header file included first at the top of the file (e.g., microsoft example?).

A modified project structure and accompanying source files are shown below. The pch source file is moved to a subdirectory so that it will not be automatically added to the source list. The pch header file (include/core/pch.h) contains straightforward includes and is included as the first line in all source files (i.e., non-header files). The pch header file will work whether or not using precompiled headers.

By moving the pch source file to a subdirectory, the pch source file will not be automatically added to the source list (i.e., won't be found by Glob).

Meanwhile, the way SCons includes the pch file is the msvc way: /Yu to use it and /Fp to specify the filename. Then it acts like a #include had been placed in the files being compiled this way - similar to how gcc's --include works - and interestingly, SCons can't handle that, since it depends on scanners to work out the dependency chain, and the scanner doesn't know about an include provided by the compiler that doesn't appear in the source code.

The generated pch file is a binary which is just telling the compiler which headers have already been processed so it can skip processing certain header includes and use the information from the binary. As in the microsoft examples, the normal includes are still present. In the modified layout below, all headers from the source files and source pch include file should be discoverable. In the current example, all of the included headers are discoverable from the source.

The relationship between the pch header file and the generated pch binary file is the missing link (which is what I think you're getting at). This relationship can't be determined automatically by scanning the source files. The SCons PCH() method provides the mechanism to generate the pch binary file as a side-effect of compiling the pch source file and provides the necessary dependency relationship to the generated pch file.

Modified Project Files

File archive: test-2249-jcb.zip

Project layout:

project_root
+--include
|  +--core
|     Hello.hpp   # <-- local header
|     pch.hpp     # <-- pch header file for source files
+--lib
|  +--core
|     +--pch      # <-- subfolder for pch source file
|        pch.cpp  # <-- pch source file (include pch.hpp)
|     Hello.cpp   # <-- include "core/pch.hpp"
|     SConscript
|  SConstruct

Source Files:

  • include/core/Hello.hpp

    #pragma once
    
    #include <string>
    
    class Hello
    {
        public:
           Hello(const std::string& msg);
           ~Hello();
           void sayIt();
         
       private:
        s  td::string m_msg; 
    };
    
  • include/core/pch.hpp

    Note: "stable" includes, works with or without PCH enabled.

    #pragma once
    
    #include <string>          # <-- standard header
    #include <iostream>        # <-- standard header
    
    #include "core/Hello.hpp"  # <-- stable local header
    
  • lib/core/pch/pch.cpp

    #include "core/pch.hpp"  # <-- pch header
    
  • lib/core/Hello.cpp

    #include "core/pch.hpp"  # <-- pch header (must be first)
    
    Hello::Hello(const std::string& msg):m_msg(msg)
    {
    }
    
    Hello::~Hello()
    {
    }
    
    void Hello::sayIt()
    {
        std::cout << m_msg << std::endl;
    }
    
  • lib/core/SConscript

    Note: due to pch subfolder, the pch source file will not be added to source list.

    #get all the build variables we need
    Import('env', 'buildroot', 'project', 'mode', 'debugcflags', 'releasecflags', 'usepch')
    staticlibenv = env.Clone()
    
    builddir = buildroot + '/' + project   #holds the build directory for this project
    targetpath = builddir  #holds the path to the executable in the build directory
    
    #append the user's additional compile flags
    #assume debugcflags and releasecflags are defined
    if mode == 'debug':
       staticlibenv.Append(CPPFLAGS=debugcflags)
    else:
       staticlibenv.Append(CPPFLAGS=releasecflags)
    
    #get source file list
    srclst = Glob('*.cpp')
    
    staticlibenv2 = staticlibenv.Clone()
    
    if usepch:
    
        pch_nodes = staticlibenv2.PCH('pch/pch.cpp')
        staticlibenv2['PCHSTOP'] = 'core/pch.hpp'
        staticlibenv2['PCH'] = pch_nodes[0]
    
        additionalcflags = ['-Yl__my_pch_symbol']
        staticlibenv2.Append(CPPFLAGS=additionalcflags)
    
    staticlib = staticlibenv2.StaticLibrary("core", source=srclst)
    
  • SConstruct

    usepch = True  # <-- pch create/use boolean
    ...
    Export('env', 'mode', 'platform', 'debugcflags', 'releasecflags', 'usepch')  # <-- export pch boolean
    

Generated compile commands:

# compile pch.cpp -> pch.obj, pch.pch
cl /Fobuild\debug\lib\core\static\pch\pch.obj /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -Yl__my_pch_symbol /Iinclude /c build\debug\lib\core\static\pch\pch.cpp /Yccore/pch.hpp /Fpbuild\debug\lib\core\static\pch\pch.pch

# compiler Hello.c -> Hello.obj
cl /Fobuild\debug\lib\core\static\Hello.obj /c build\debug\lib\core\static\Hello.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -Yl__my_pch_symbol /Iinclude /Yucore/pch.hpp /Fpbuild\debug\lib\core\static\pch\pch.pch

# create static library
lib /nologo /OUT:build\debug\lib\core\static\core.lib build\debug\lib\core\static\Hello.obj

Project layout (after PCH build):

project_root
+--build
|  +--debug
|     +--lib
|        +--core
|           +--static
|              +--pch
|                 pch.cpp  # <-- pch source file (include pch.hpp)
|                 pch.obj  # <-- pch object file
|                 pch.pch  # <-- generated pch file
|              core.lib
|              Hello.cpp
|              Hello.obj
|              SConscript
+--include
|  +--core
|     Hello.hpp
|     pch.hpp
+--lib
|  +--core
|     +--pch
|        pch.cpp
|     Hello.cpp
|     SConscript   
|  .sconsign.dblite
|  SConstruct

@mwichmann
Copy link
Collaborator

Cherry picking what to reply to because I just found something new:

I can see where precompiled headers would be valuable when using "heavyweight" third-party c++ libraries especially those with a ton of template metaprogramming and/or template specialization. For a local project, typically the third-party libraries would not be updated frequently. For example, c++ libraries like QT, wxWidgets, OGRE, Boost, etc., ....

Yes, this is the intent afaict.

In most of the examples, the pch header file is processed in its entirety so there is no reason to include the pch object file in the linker command.

According to a comment in the code (specifically, in the linker tool), the object file does have to be included, so it arranges to do so automatically:

# MSVC 11 and above need the PCH object file to be added to the link line,

I wonder if this automatic inclusion may contribute to the problem we're chasing our tails on?

@jcbrill
Copy link
Contributor

jcbrill commented Nov 7, 2023

According to a comment in the code (specifically, in the linker tool), the object file does have to be included, so it arranges to do so automatically:

Well, that is unexpected.

I don't think it applies to the examples thus far as they are building a StaticLibrary which should be using scons/SCons/Tool/mslib.py. Correct?

In the runs thus far;

  • When the pch source file is included in the source list, the pch object file appears on the lib command-line.
  • When the pch source file is not present in the source list, the pch object file does not appear on the lib command-line.

Perhaps it should be changed and tested for a SharedLibrary?

@jcbrill
Copy link
Contributor

jcbrill commented Nov 7, 2023

@jcbrill
Copy link
Contributor

jcbrill commented Nov 7, 2023

Old but useful: https://stackoverflow.com/questions/35408988/lnk2011-why-does-link-exe-require-stub-obj-to-be-linked-alongside-precompiled-h

I'm starting to wonder if there could be a problem when linking with created static library in another program since the pch object file is not included when building the static library. Any thoughts?

I tested the modified example above to generate a SharedLibrary and it does in fact include the pch object file when linking.

@jcbrill
Copy link
Contributor

jcbrill commented Nov 8, 2023

@mwichmann and @bdbaddog PCH generation issue and possible code change solution explained.

Description of terms used below:

  • PCH.cpp: pch source file used to produce the precompiled header binary and object file
  • PCH.pch: generated precompiled header binary
  • PCH.obj: generated object file for pch source file

The mermaid diagrams are rendered when viewing the GitHub html page.

Observed Behavior

From above in this thread:

There is an issue when both the environment PCH method is called and the pch source file is included in the source file list that can result in build failure.

Summary:

  • When the pch source file is not in the source file list, the build is successful.
  • When the pch source file is in the source file list, the build may fail.

The reason why the build may succeed in some configurations and fail in other configurations when the pch source file is in the source list is described in the next section.

Issue Description and Illustration

When both the environment PCH method is called and the pch source file is included in the source file list, the PCH.obj node has two builders/emitters assigned during construction with the second builder/emitter overwriting the original builder/emitter.

This is undesirable as a taskmaster evaluation criteria has been removed and the lexical ordering of nodes affects the evaluation order during the DAG walk. The DAG walk and evaluation order is not deterministic. Builds that are successful, succeed for the wrong reason.

SConscript fragment that results in State A and State B in the diagrams below (some build configurations may fail):

# PCH_SRC is in srclist
env.PCH(PCH_CPPFILE)
env.StaticLibrary("core", source=srclist)

SConscript fragment that results in State A, State B, and State C in the diagrams below (all build configurations pass):

# PCH_SRC is in srclist
env.PCH(PCH_CPPFILE)
env.StaticLibrary("core", source=srclist)
env.PCH(PCH_CPPFILE)

The second call to the environment PCH method resolves the issue and is presented for exposition purposes only. However, the behavior informs the proposed solution below.

Node PCH.pch and PCH.obj states:

  • State A: env.PCH('PCH.cpp')

    • msvc.pch_emitter(['PCH.pch'], ['PCH.cpp'])
    flowchart LR
      subgraph 1 ["State A: env.PCH('PCH.cpp') => msvc.pch_emitter"]
      direction LR
      N1[PCH.pch] --> B1["Builder $PCHCOMSTR"] --> E1["Executor $CXX ... <br> target=[PCH.pch, PCH.obj], source=[PCH.cpp]"]
      N2[PCH.obj] --> B1
      end
    
    Loading

    The environment PCH method causes the pch emitter to be assigned to both the PCH.pch and PCH.obj nodes.

    The first target for the PCH.pch and PCH.obj nodes is PCH.pch (e.g., target[0]==PCH.pch).

  • State B: StaticLibrary('core', source=['PCH.cpp', 'Hello.cpp'])

    • msvc.static_object_emitter(['PCH.obj'], ['PCH.cpp'])
    flowchart LR
      subgraph 2 ["State B: StaticLibrary('core', source=['PCH.cpp', 'Hello.cpp']) => msvc.static_object_emitter"]
      direction LR
      N3[PCH.pch] --> B2["Builder $PCHCOMSTR"] --> E2["Executor $CXX ... <br> target=[PCH.pch, PCH.obj], source=[PCH.cpp]"]
      N4[PCH.obj] --> B3["Builder CXXAction"] --> E3["Executor $CXX ... <br> target=[PCH.OBJ], source=[PCH.cpp]"]
      end
    
    Loading

    The environment StaticLibrary method causes the static object emitter to be assigned to the PCH.obj node.

    The first target for the PCH.obj node is now PCH.obj (e.g., target[0]==PCH.obj) and not PCH.pch.

    The target[0] change influences the DAG walk evaluation order and is the root cause of the build failures. However, even in the builds that succeed, they are really succeeding for the wrong reason.

    PCH.obj has lost it's primary target of PCH.pch when the builder/executor is overwritten.

    The target[0] change allows the PCH.obj node to be executed before the PCH.pch node is executed in some cases based on the lexical ordering of nodes.

    This is seen in the build failures for PCH.obj as the command issued is to use the pch file rather than create the pch file.

  • [Optional] State C: env.PCH('PCH.cpp')

    • msvc.pch_emitter(['PCH.pch'], ['PCH.cpp'])
    flowchart LR
      subgraph 3 ["State C: env.PCH('PCH.cpp') => msvc.pch_emitter"]
      direction LR
      N5[PCH.pch] --> B4["Builder $PCHCOMSTR"] --> E4["Executor $CXX ... <br> target=[PCH.pch, PCH.obj], source=[PCH.cpp]"]
      N6[PCH.obj] --> B4
      end
    
    Loading

    Calling the env PCH method a second time after the environment StaticLibrary call restores the builder/executor for PCH.obj.

    The first target for the PCH.pch and PCH.obj nodes is PCH.pch (e.g., target[0]==PCH.pch).

Can the dual assignment of build/emitter to PCH.obj be prevented? The possible solution below appears to solve this problem.

Possible Solution

SCons.Tool.msvc.object_emitter:

  • Current code

    def object_emitter(target, source, env, parent_emitter):
        """Sets up the PCH dependencies for an object file."""
    
        validate_vars(env)
    
        parent_emitter(target, source, env)
    
        # Add a dependency, but only if the target (e.g. 'Source1.obj')
        # doesn't correspond to the pre-compiled header ('Source1.pch').
        # If the basenames match, then this was most likely caused by
        # someone adding the source file to both the env.PCH() and the
        # env.Program() calls, and adding the explicit dependency would
        # cause a cycle on the .pch file itself.
        #
        # See issue #2505 for a discussion of what to do if it turns
        # out this assumption causes trouble in the wild:
        # https://github.com/SCons/scons/issues/2505
        pch=get_pch_node(env, target, source)
        if pch:
            if str(target[0]) != SCons.Util.splitext(str(pch))[0] + '.obj':
                env.Depends(target, pch)
    
        return (target, source)
    
  • Possible solution code (annotated):

    def object_emitter(target, source, env, parent_emitter):
        """Sets up the PCH dependencies for an object file."""
    
        validate_vars(env)
    
        parent_emitter(target, source, env)
    
        # Add a dependency, but only if the target (e.g. 'Source1.obj')
        # doesn't correspond to the pre-compiled header ('Source1.pch').
        # If the basenames match, then this was most likely caused by
        # someone adding the source file to both the env.PCH() and the
        # env.Program() calls, and adding the explicit dependency would
        # cause a cycle on the .pch file itself.
        #
        # See issue #2505 for a discussion of what to do if it turns
        # out this assumption causes trouble in the wild:
        # https://github.com/SCons/scons/issues/2505
        pch=get_pch_node(env, target, source)
        if pch:
            pch_basename = SCons.Util.splitext(str(pch))[0]  # <-- save the basename
            if str(target[0]) != pch_basename + '.obj':      # <-- use the basename
                env.Depends(target, pch)
            elif str(source[0]) == pch_basename + '.cpp':    # <-- use the basename
                # target == PCH.obj and source == PCH.cpp
                # don't overwrite PCH builder for PCH.obj
                target = None                                # <-- invalidate target list
                source = None                                # <-- invalidate source list
    
        return (target, source)
    

    When the object_emitter is called for target PCH.obj and source PCH.cpp, the target list and source list is invalidated (i.e., both are set to None) which suppresses the assignment of the static object builder/emitter to the PCH.obj node (i.e., the pch builder/emitter is not overwritten).

    By maintaining the original pch builder/emitter assignment for PCH.obj, the DAG walk evaluation order is deterministic which is desirable.

    The PCH.pch node is executed before the PCH.obj node is executed.

Manual test results for proposed code:

Build Kind PCH.cpp Hellp.CPP Rem PCH.CPP Result First Node Output
1 static a-pch.cpp b-hello.cpp False Pass a-pch.pch a-pch.pch, a-pch.obj
2 shared a-pch.cpp b-hello.cpp False Pass a-pch.pch a-pch.pch, a-pch.obj
3 static b-pch.cpp a-hello.cpp False Pass b-pch.pch b-pch.pch, b-pch.obj
4 shared b-pch.cpp a-hello.cpp False Pass b-pch.pch b-pch.pch, b-pch.obj
5 static a-pch.cpp b-hello.cpp True Pass a-pch.pch a-pch.pch, a-pch.obj
6 shared a-pch.cpp b-hello.cpp True Pass a-pch.pch a-pch.pch, a-pch.obj
7 static b-pch.cpp a-hello.cpp True Pass b-pch.pch b-pch.pch, b-pch.obj
8 shared b-pch.cpp a-hello.cpp True Pass b-pch.pch b-pch.pch, b-pch.obj

With the proposed code change:

  • All manual tests result in the PCH.pch node being evaluated first which produces the PCH.pch and PCH.obj files regardless of the PCH.pch name and any lexical ordering as the initial PCH.obj target list from the PCH builder/emitter is preserved.

  • The automated test scons/test/MSVC/PCH-source.py passes regardless of source file order.

  • The automated tests for MSVC/MSVC (79 tests) all pass or have no result.

@mwichmann
Copy link
Collaborator

Sorry I haven't had a chance to really look at this in detail yet, but the analysis looks on point. I did put up a PR to the docs last week that suggested not including the "pch source file" as a source elsewhere, as SCons will track it correctly from there. I presume that's really not enough of a "solution"? The previously referenced issue #2505 did suggest limited confidence that the fix applied there was entirely correct, which has proven to be true, since this detail of dueling emitters was not recognized.

@jcbrill
Copy link
Contributor

jcbrill commented Nov 21, 2023

No worries.

The proposed solution above was tweaked a little and is now available for testing in PR #4444.

In limited testing, it appears to "do the right thing".

The PR passes for the 16 combinations of the four pairs:

  • File name order: (a-pch.cpp, b-hello.cpp) vs (b-pch.cpp, a-hello.cpp)
  • PCH.cpp is in the source file list: True vs False
  • The env.PCH() call is before (or after) the StaticLibrary/SharedLibrary call: True vs False
  • Library kind: StaticLibrary vs SharedLibrary

The PCH-source test was modified to have "foo.cpp" at the end of the list which used to fail.

It might not be the worst idea to mention than there can only be one pch node per environment in addition to the requirement that the pch source file must be c++.

@mwichmann
Copy link
Collaborator

Now that that's in a published version, I'm happy to receive concrete suggestions for improvement (which I will undoubtedly tweak further).... https://scons.org/doc/production/HTML/scons-man.html#b-PCH

@bdbaddog
Copy link
Contributor Author

@jcbrill - sorry for the delay in reviewing this and the PR.
So your suggested solution is to fix this under the hood and just make it work?

What happens if env['PCH'] is set, but the user never calls env.PCH(...) ?

@jcbrill
Copy link
Contributor

jcbrill commented Nov 30, 2023

sorry for the delay in reviewing this and the PR.

No need to apologize. I'm sorry for going missing the last few days.

So your suggested solution is to fix this under the hood and just make it work?

Yes.

What happens if env['PCH'] is set, but the user never calls env.PCH(...) ?

That is a really good question.

Without the env.PCH(...) call, the output pch file will not be generated and the build fails.

In the current implementation, there are different reasons for the build failure based on whether or not the pch source file is removed from the source file list:

  • When the pch source file is in the source file list, there is a c compiler error.
  • When the pch source file is not in the source file list, there is an scons error.

In the suggested solution, there is an scons error regardless of whether the pch source file is in the source file list or not:

  • When the pch source file is in the source file list, there is an scons error.
  • When the pch source file is not in the source file list, there is an scons error.

One could argue that it is more desirable to fail with an scons error rather than a c compiler error.

The results of the four runs are shown below based on the following combinations: (master, branch) x (pch source file included, pch source file removed).

Test results:

  • master (scons-master)

    • pch source file included in source list: c compiler error

      fatal error C1083: Cannot open precompiled header file: 'build\debug\lib\core\static\a-pch.pch': No such file or directory

      scons: Reading SConscript files ...
      **** Compiling in debug mode...
      PCH_FILE_REMOVE = False
      scons: done reading SConscript files.
      scons: Building targets ...
      cl /Fobuild\debug\lib\core\static\a-pch.obj /c build\debug\lib\core\static\a-pch.cpp /TP /nologo -Od -MDd -Ob0 -Z7 -W4 -EHsc -GR -D_DEBUG -DUSE_PCH -Yl__xge_pch_symbol /Iinclude /Yucore/CorePCH.hpp /Fpbuild\debug\lib\core\static\a-pch.pch
      a-pch.cpp
      build\debug\lib\core\static\a-pch.cpp(1): fatal error C1083: Cannot open precompiled header file: 'build\debug\lib\core\static\a-pch.pch': No such file or directory
      scons: *** [build\debug\lib\core\static\a-pch.obj] Error 2
      scons: building terminated because of errors.
      
    • pch source file not in source list: scons error

      scons: *** [build\debug\lib\core\static\b-hello.obj] Explicit dependency 'build\debug\lib\core\static\a-pch.pch' not found, needed by target 'build\debug\lib\core\static\b-hello.obj'

      scons: Reading SConscript files ...
      **** Compiling in debug mode...
      PCH_FILE_REMOVE = True
      scons: done reading SConscript files.
      scons: Building targets ...
      scons: *** [build\debug\lib\core\static\b-hello.obj] Explicit dependency `build\debug\lib\core\static\a-pch.pch' not found, needed by target `build\debug\lib\core\static\b-hello.obj'.`
      scons: building terminated because of errors.
      
  • branch (jbrill-msvc-pchnode)

    • pch source file included in source list: scons error

      scons: *** [build\debug\lib\core\static\b-hello.obj] Explicit dependency 'build\debug\lib\core\static\a-pch.pch' not found, needed by target 'build\debug\lib\core\static\b-hello.obj'

      scons: Reading SConscript files ...
      **** Compiling in debug mode...
      PCH_FILE_REMOVE = False
      scons: done reading SConscript files.
      scons: Building targets ...
      scons: *** [build\debug\lib\core\static\b-hello.obj] Explicit dependency `build\debug\lib\core\static\a-pch.pch' not found, needed by target `build\debug\lib\core\static\b-hello.obj'.
      scons: building terminated because of errors.
      
    • pch source file not included in source list: scons error

      scons: *** [build\debug\lib\core\static\b-hello.obj] Explicit dependency 'build\debug\lib\core\static\a-pch.pch' not found, needed by target 'build\debug\lib\core\static\b-hello.obj'

      scons: Reading SConscript files ...
      **** Compiling in debug mode...
      PCH_FILE_REMOVE = True
      scons: done reading SConscript files.
      scons: Building targets ...
      scons: *** [build\debug\lib\core\static\b-hello.obj] Explicit dependency `build\debug\lib\core\static\a-pch.pch' not found, needed by target `build\debug\lib\core\static\b-hello.obj'.
      scons: building terminated because of errors.
      

EDIT: ADDENDUM

In the current implementation, the names of the source files matter.

In the case above (a-pch.pch, b-hello.cpp) there are different errors.

For (b-pch.cpp, a-hello.cpp), there is only the scons dependency error.

In the suggested solution, the same error is produced regardless of source file naming.

@mwichmann
Copy link
Collaborator

What happens if env['PCH'] is set, but the user never calls env.PCH(...) ?

Well, in this case (rephrasing from the previous comment) - you haven't given SCons the information of how a PCH file fits into the plumbing, so it shouldn't be expected to work. If the implementation were different, it might theoretically work if the PCH file were already present in the source tree, SCons just wouldn't know how to regenerate it, but since Microsoft also requires the "PCH object file" (don't even know what to call that) to be part of the link, it can't work. I guess one could ask - should env['PCH'] even have any meaning if env.PCH() hasn't been called, and just get ignored? But we don't have to solve every detail...

@bdbaddog
Copy link
Contributor Author

What happens if env['PCH'] is set, but the user never calls env.PCH(...) ?

Well, in this case (rephrasing from the previous comment) - you haven't given SCons the information of how a PCH file fits into the plumbing, so it shouldn't be expected to work. If the implementation were different, it might theoretically work if the PCH file were already present in the source tree, SCons just wouldn't know how to regenerate it, but since Microsoft also requires the "PCH object file" (don't even know what to call that) to be part of the link, it can't work. I guess one could ask - should env['PCH'] even have any meaning if env.PCH() hasn't been called, and just get ignored? But we don't have to solve every detail...

It might be dumb, but the no PCH() should work. the pch.cpp file would basically be a no-op right?

@mwichmann
Copy link
Collaborator

mwichmann commented Nov 30, 2023

I get more confused the more time passes. We do this in various tests, so we're setting both (as you'd expect).

env['PCH'] = env.PCH('Precompiled.cpp')[0]

Meanwhile, there's a routine get_pch_node that basically fishes out the node for PCH, and mslink uses that to deduce the object file name and add it to the dllEmitter and progEmitter, so I think it's not a no-op.

@jcbrill
Copy link
Contributor

jcbrill commented Nov 30, 2023

Meanwhile, there's a routine get_pch_node that basically fishes out the node for PCH, and mslink uses that to deduce the object file name and add it to the dllEmitter and progEmitter, so I think it's not a no-op.

This. msvc also calls get_pch_node in the object emitter.

Once the pch node has been defined, the compile commands will include the pch header in the command-line (e.g., /Yucore/CorePCH.hpp ) which would fail if there was not an explicit dependency error.

I'm not sure there is a reliable way to know if env.PCH() has been called and when it was called (before or after the env['PCH'] assignment).

@mwichmann
Copy link
Collaborator

I wonder if the idea is to say not to set the PCH consvar manually, and instead have the PCH builder be the one that sets it.

@jcbrill
Copy link
Contributor

jcbrill commented Nov 30, 2023

It appears that calling env.PCH() and not defining env['PCH'] fails with a python error (at least in the suggested version).

However, there is a non-trivial chance that the test configuration is completely messed up.

I don't have time to investigate further tonight, but will tomorrow morning.

@jcbrill
Copy link
Contributor

jcbrill commented Dec 1, 2023

I wonder if the idea is to say not to set the PCH consvar manually, and instead have the PCH builder be the one that sets it.

The suggested solution was adjusted to set env['PCH']=pch in the msvc pch_emitter when either env['PCH'] is undefined or evaluates to false.

A set of 24 experiments were run using the 4.6.0ish main branch and the suggested solution jbrill-msvc-pchnode. The results of the experiments are shown below.

Comments:

  • calling env.PCH() without setting env['PCH'] results in python errors in the current implementation in some cases. The taskmaster eventually tries to call a node's executor method when the node's executor is None.
  • setting env['PCH'] without calling env.PCH() results in c errors and scons errors in the current implementation.
  • setting env['PCH'] without calling env.PCH() results in scons errors in the currrent suggested implementation.

At present, I'm not sure that setting env['PCH'] without calling env.PCH() can be turned into a no-op.

Experiment Results

Column legend for tables:

  • pchsrch (pch source file):
    • a-pch: a-pch.cpp, b-hello.cpp
    • b-pch: b-pch.cpp, a-hello.cpp
  • remsrc (remove pch source file from source file list):
    • False: pch source file is in source file list
    • True: pch source file is not in source file list
  • pchcall (call env.PCH()):
    • False: do not call env.PCH()
    • True: call env.PCH()
  • before (call env.PCH() before env['PCH'] assignment, if any):
    • n/a: does not apply, env.PCH() not called
    • False: env.PCH() is called after env['PCH'] assignment, if any
    • True: env.PCH() is called before env['PCH'] assignment, if any
  • envpch (assign env['PCH']):
    • False: do not assign env['PCH']
    • True: assign env['PCH']

NOTE: Pass does not mean that precompiled headers are being used, simple that the library build successfully.

Branch 4.6.0 (total: 24, fail c: 3, fail python: 4, fail scons: 3, pass: 14):

id pchsrc remsrc pchcall before envpch result
1 a-pch False False n/a False Pass
2 a-pch False False n/a True Fail: C1083: Cannot open precompiled header file
3 a-pch False True False False Fail: AttributeError: 'NoneType' object has no attribute 'get_build_env'
4 a-pch False True False True Fail: C1083: Cannot open precompiled header file
5 a-pch False True True False Fail: AttributeError: 'NoneType' object has no attribute 'get_build_env'
6 a-pch False True True True Fail: C1083: Cannot open precompiled header file
7 a-pch True False n/a False Pass
8 a-pch True False n/a True Fail: Explicit dependency
9 a-pch True True False False Pass
10 a-pch True True False True Pass
11 a-pch True True True False Pass
12 a-pch True True True True Pass
13 b-pch False False n/a False Pass
14 b-pch False False n/a True Fail: Explicit dependency
15 b-pch False True False False Fail: AttributeError: 'NoneType' object has no attribute 'get_build_env'
16 b-pch False True False True Pass
17 b-pch False True True False Fail: AttributeError: 'NoneType' object has no attribute 'get_build_env':
18 b-pch False True True True Pass
19 b-pch True False n/a False Pass
20 b-pch True False n/a True Fail: Explicit dependency
21 b-pch True True False False Pass
22 b-pch True True False True Pass
23 b-pch True True True False Pass
24 b-pch True True True True Pass

Branch jbrill-msvc-pchnode (total: 24, fail c: 0, fail python: 0, fail scons: 4, pass: 20):

id pchsrc remsrc pchcall before envpch result
1 a-pch False False n/a False Pass
2 a-pch False False n/a True Fail: Explicit dependency
3 a-pch False True False False Pass
4 a-pch False True False True Pass
5 a-pch False True True False Pass
6 a-pch False True True True Pass
7 a-pch True False n/a False Pass
8 a-pch True False n/a True Fail: Explicit dependency
9 a-pch True True False False Pass
10 a-pch True True False True Pass
11 a-pch True True True False Pass
12 a-pch True True True True Pass
13 b-pch False False n/a False Pass
14 b-pch False False n/a True Fail: Explicit dependency
15 b-pch False True False False Pass
16 b-pch False True False True Pass
17 b-pch False True True False Pass
18 b-pch False True True True Pass
19 b-pch True False n/a False Pass
20 b-pch True False n/a True Fail: Explicit dependency
21 b-pch True True False False Pass
22 b-pch True True False True Pass
23 b-pch True True True False Pass
24 b-pch True True True True Pass

Edits:

  • fixed error in comments above (3rd item: current changed to suggested)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants