Search code examples
perlpackage

How to define multiple Perl packages in one file that are inter-dependent


I've tried following some other examples and have something like:

{
    package Foo::Bar;
}
{
    package Foo::Baz;

    use Foo::Bar;
}

use Foo::Baz;
# some more code 

# Fails with: Can't locate Foo/Bar.pm in @INC 

My real-world example is I'd like to bundle/concat https://github.com/TeX-Live/texlive-source/blob/trunk/texk/texlive/linked_scripts/texlive/fmtutil.pl along with its dependencies https://github.com/TeX-Live/installer/blob/master/tlpkg/TeXLive/TLConfig.pm and https://github.com/TeX-Live/installer/blob/master/tlpkg/TeXLive/TLUtils.pm into a single file.

Thank you!


Solution

  • The problem is that Perl doesn't know the module is already loaded because you haven't faithfully replicated the loading process. Specifically, you didn't modify %INC.

    You could also have problems from not loading the module at compile-time. This could be achieved using a BEGIN block.

    To inline a module, add the following to the start of your script:

    BEGIN {
       # Insert module here.
    
       $INC{ ( __PACKAGE__ =~ s{::}{/}rg ) . ".pm" } = 1;
    }
    

    So if you had

    # script.pl
    use strict;
    use Foo::Baz;
    # ...
    
    # Foo/Bar.pm
    package Foo::Bar;
    use strict;
    # ...
    1;
    
    # Foo/Baz.pm
    package Foo::Baz;
    use strict;
    use Foo::Bar;
    # ...
    1;
    

    You'd end up with

    BEGIN {
       # Foo/Bar.pm
       package Foo::Bar;
       use strict;
       # ...
       $INC{ ( __PACKAGE__ =~ s{::}{/}rg ) . ".pm" } = 1;
    }
    
    BEGIN {
       # Foo/Baz.pm
       package Foo::Baz;
       use strict;
       use Foo::Bar;
       # ...
       $INC{ ( __PACKAGE__ =~ s{::}{/}rg ) . ".pm" } = 1;
    }
    
    # script.pl
    use strict;
    use Foo::Baz;
    # ...
    

    Note the above isn't 100% equivalent to inlining the modules. For example, the equivalent of

    use 5.012;
    use open ":std", ":encoding(UTF-8)";
    use Some::Module;
    

    would actually be

    # Non-lexical effects
    BEGIN {
       require 5.012;
       binmode(STDIN,  ":encoding(UTF-8)");
       binmode(STDOUT, ":encoding(UTF-8)");
       binmode(STDERR, ":encoding(UTF-8)");
    }
    
    BEGIN {
       package Some::Module;
       ...
       $INC{"Some/Module.pm"} = 1;
    }
    
    # Lexical effects
    use 5.012;
    use open ":encoding(UTF-8)";
    

    To properly inline a module, it would be better to use an @INC hook. Using the files from the first approach, one would end up with

    BEGIN {
       my %modules = (
          "Foo/Bar.pm" => <<'__EOI__',
    # Foo/Bar.pm
    package Foo::Bar;
    use strict;
    # ...
    1;
    __EOI__
          "Foo/Baz.pm" => <<'__EOI__',
    # Foo/Baz.pm
    package Foo::Baz;
    use strict;
    use Foo::Bar;
    # ...
    1;
    __EOI__
       );
    
       unshift @INC, sub {
          my $module = $modules{$_[1]}
             or return;
    
          return \$module;
       };
    }
    
    # script.pl
    use strict;
    use Foo::Baz;
    # ...
    

    App::FatPacker can be used to inline modules this way.

    The line numbers in warnings and errors will be the line numbers of the original file. a #line directive added to the inlined module would adjust that.